Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2e939b0
Added command which collects a list of deprecated functions and classes
SamBoyd Nov 22, 2022
c5d0bd6
Refactor to test
SamBoyd Nov 22, 2022
ba03898
Init command now searches the codebase for usages of the deprecated code
SamBoyd Nov 25, 2022
2d21c2e
Added test cli command to test if there have been added usages to dep…
SamBoyd Nov 25, 2022
6f344b9
Added Config class to replace dictionary used to hold the shitlist co…
SamBoyd Nov 25, 2022
df7c724
Added update cli command to update the config to represent the curren…
SamBoyd Nov 25, 2022
6aee01f
Remove cli_2 command
SamBoyd Nov 26, 2022
eb0735e
Added ability to ignore certain directories
SamBoyd Nov 27, 2022
4f73666
Dont overwrite an already initialized config file
SamBoyd Nov 27, 2022
b7ee2fb
Logger logs to the terminal correctly
SamBoyd Nov 27, 2022
f76230e
Fleshed out a bit of the readme
SamBoyd Nov 27, 2022
074fb78
Added logo to readme
SamBoyd Nov 27, 2022
2b2cc95
The deprecate decorator now takes a string to deifne the alternative …
SamBoyd Dec 26, 2022
6c7bbfc
Refactor - renamed deprecated_thing to deprecated_code
SamBoyd Dec 29, 2022
cb384d1
Explicitly defining packages in the build config to stop setuptools t…
SamBoyd Jan 1, 2023
1795bd6
Added some helpful logging to the test cli command
SamBoyd Jan 1, 2023
297b58c
When running the test command display a warning if there is newly dep…
SamBoyd Jan 1, 2023
cf6de99
Freezing requirements versions
SamBoyd Jan 2, 2023
7962ef5
Refactor to pull the config filepath to a local variable
SamBoyd Jan 2, 2023
74b8454
Added first iteration of a progress chart to show the team how much o…
SamBoyd Jan 2, 2023
49bc9a3
Fixed bug where removed usages and successfully removed code wasnt be…
SamBoyd Jan 2, 2023
5a7bcfb
Fixed typo
SamBoyd Jan 2, 2023
762fff6
Remove file throwing off pytest
SamBoyd Jan 14, 2023
4389898
Add pytest section to pyproject.toml, setting up PYTHONPATH
SamBoyd Jan 14, 2023
c06a70b
Skipping tests that arent implemented yet
SamBoyd Jan 14, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.idea
dist
.python-version
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) [year] [fullname]

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
50 changes: 49 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,49 @@
# Shitlist

![Shitlist](https://github.com/samboyd/shitlist/assets/logo.svg?raw=true)

---

Shitlist is a deprecation tool for python. It checks that deprecated code is only lessened in your codebase by
testing that any new code doesn't use already deprecated code.

Add shitlist to your build script to check that deprecated code is gradually remove from your codebase.

Inspired by a post by Simon Eskildsen, [Shitlist driven development (2016)](https://sirupsen.com/shitlists)

## Installation



```bash
pip install shitlist
```


## Usage

Initialize the config file. This will create a file `.shitlist` in the project root directory
```bash
shitlist init
```

Deprecate some code
```python
import shitlist

@shitlist.deprecate(
alternative='You should use `new_function` because of X, Y & Z'
)
def old_function():
pass
```

Update the config file with the newly deprecated code. Shitlist will look for usages of all your deprecated code
and update the config file
```bash
shitlist update
```

Test
```bash
shitlist test
```
Empty file added __init__.py
Empty file.
Binary file added assets/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

[project]
name = "shitlist"
version = "0.0.1"
authors = [
{ name="Sam Boyd", email="samboyd10@gmail.com" },
]
description = "Shitlist deprecation"
readme = "README.md"
requires-python = ">=3.7"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]

[project.urls]
"Homepage" = "https://github.com/samboyd/shitlist"
"Bug Tracker" = "https://github.com/samboyd/shitlist/issues"

[project.scripts]
"shitlist"= "shitlist.cli:main"

[tool.setuptools]
packages = ["shitlist"]

[tool.pytest.ini_options]
pythonpath = [
"."
]
addopts = "--ignore=dist --ignore=assets"
15 changes: 15 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
attrs==22.1.0
build==0.9.0
click==8.1.3
exceptiongroup==1.0.0
freezegun==1.2.2
iniconfig==1.1.1
packaging==21.3
pep517==0.13.0
PyHamcrest==2.0.4
pyparsing==3.0.9
pytest==7.2.0
pytest-mock==3.10.0
python-dateutil==2.8.2
six==1.16.0
tomli==2.0.1
203 changes: 203 additions & 0 deletions shitlist/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import ast
import enum
import logging
import os
from pathlib import PosixPath
from typing import List, Callable, Optional

from shitlist.config import Config
from shitlist.decorator_use_collector import DecoratorUseCollector
from shitlist.deprecated_code_use_collector import DeprecatedCodeUseCollector

ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
logger = logging.getLogger(__name__)


class DeprecatedException(Exception):
pass


class UndefinedAlternativeException(Exception):
pass


class ErrorLevel(enum.Enum):
error = 'error'
warn = 'warn'


usages = []
error_level = ErrorLevel.error


class WrongTypeError(Exception):
pass


def get_func_name(func: Callable):
filepath = func.__code__.co_filename
filepath = filepath.replace(ROOT_DIR, '')
func_name = func.__qualname__
return f'{filepath}::{func_name}'


def deprecate(alternative: str):
if alternative is None:
raise UndefinedAlternativeException("alternative code not defined")

def wrapped_deprecate(func):
# wrap a function
if type(func).__name__ == 'function':
def wrapper():
if get_func_name(func) not in usages:
if error_level == ErrorLevel.error:
raise RuntimeError()
else:
func_name = func.__qualname__
logger.info(
f'function {func_name} is registered on a shitlist and therefore should not'
f' be used by new code'
)
func()

wrapper.shitlist_deprecate = True
wrapper.wrapped_function = get_func_name(func)

return wrapper
else:
raise WrongTypeError()

return wrapped_deprecate


def gen_for_path(
root_path: PosixPath,
ignore_directories=[]
):
result = set()
walker = TreeWalker(
root_dir=root_path,
ignore_directories=ignore_directories
)

while walker.has_next:
path = walker.next_file()
module_name = path.stem
collector = DecoratorUseCollector(modulename=module_name)
with open(path, 'r') as f:
collector.visit(ast.parse(f.read()))

module_relative_path = str(path).replace(str(root_path) + '/', '')
if '__init__' in module_relative_path:
module_relative_path = module_relative_path.replace('/__init__.py', '')
module_relative_path = module_relative_path.replace('.py', '').replace('/', '.')
result.update([f'{module_relative_path}::{thing}' for thing in collector.nodes_with_decorators])

return sorted(list(result))


def test(existing_config: Config, new_config: Config):
for thing in existing_config.deprecated_code:
exiting_usages = set(existing_config.usage.get(thing, []))
new_usages = set(new_config.usage.get(thing, []))
dif = new_usages.difference(exiting_usages)
if len(dif) > 0:
raise DeprecatedException(f'Deprecated function {thing}, has new usages {dif}')


class TreeWalker:
has_next: bool

def __init__(self, root_dir: PosixPath, ignore_directories: List[str] = []):
self.root_dir = root_dir
self._walker = os.walk(root_dir)
self._current_dir = None
self._current_files = []
self.has_next = True
self.ignore_directories: List[PosixPath] = [root_dir / d for d in ignore_directories]

self._gen_next()

def _gen_next(self):
try:
self._current_dir, _, files = next(self._walker)
self._current_files = [f for f in files if f[-3:] == '.py'] # TODO could error on short named files
if not self._current_files or self.directory_should_be_ignored(self._current_dir):
self._gen_next()
except StopIteration:
self.has_next = False

def next_file(self) -> Optional[PosixPath]:
if self.has_next:
next_file = self._current_files.pop()
full_path = PosixPath(self._current_dir) / next_file

if len(self._current_files) == 0:
self._gen_next()

return full_path

def directory_should_be_ignored(self, dir) -> bool:
return any([True for ig_dir in self.ignore_directories if str(ig_dir) in dir])


def find_usages(
root_path: PosixPath,
deprecated_code: List[str],
ignore_directories=[]
):
result = {}
walker = TreeWalker(
root_dir=root_path,
ignore_directories=ignore_directories
)

while walker.has_next:
path = walker.next_file()
module_name = path.stem
relative_path = str(path).replace(f'{root_path}/', '')
if module_name == '__init__':
relative_path = relative_path.replace('/__init__.py', '')

package = relative_path.replace('.py', '').replace('/', '.')

for thing in deprecated_code:
module, _, function_name = thing.rpartition('::')
module_package = module.split('.')[0]
collector = DeprecatedCodeUseCollector(
deprecated_code=function_name,
modulename=module,
package=module_package
)

with open(path, 'r') as f:
collector.visit(ast.parse(f.read()))

if thing in result:
result[thing].extend([f'{package}::{u}' for u in collector.used_at])
else:
result[thing] = [f'{package}::{u}' for u in collector.used_at]
return result


def update(existing_config: Config, new_config: Config):
merged_config = Config(
deprecated_code=new_config.deprecated_code,
usage=new_config.usage,
ignore_directories=existing_config.ignore_directories,
removed_usages=existing_config.removed_usages,
successfully_removed_things=existing_config.successfully_removed_things
)

merged_config.successfully_removed_things.extend([
t for t in existing_config.deprecated_code if t not in new_config.deprecated_code
])

for thing, new_usage in new_config.usage.items():
if thing in existing_config.usage:
existing_usage = existing_config.usage[thing]
removed_usages = [u for u in existing_usage if u not in new_usage]
if removed_usages:
merged_config.removed_usages[thing] = list(removed_usages)

return merged_config
Loading