Skip to content

Commit

Permalink
Initial commit, mvp overrides, final, not_overrides
Browse files Browse the repository at this point in the history
  • Loading branch information
gricey432 committed Jun 3, 2020
0 parents commit 49f3f97
Show file tree
Hide file tree
Showing 15 changed files with 393 additions and 0 deletions.
33 changes: 33 additions & 0 deletions .github/workflows/pythonpublish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Upload Python Package

on:
release:
types: [created]

jobs:
deploy:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.6'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel
pip install .[test]
- name: Test
run: |
pytest .
- name: Build
run: |
python setup.py sdist bdist_wheel
- name: Publish PyPI
uses: pypa/gh-action-pypi-publish@master
with:
user: __token__
password: ${{ secrets.PYPI_TOKEN }}
55 changes: 55 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Editors
.idea
29 changes: 29 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
BSD 3-Clause License

Copyright (c) 2020, Mitchell Grice
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# TiePy

Maintainable, professional Python.
TiePy is a collection of optional, progressive, build-time tools for writing more maintainable Python.

## Features

* `@overrides`, `@final`, and `not_overrides` decorators

## Usage

```bash
tiepy .
```
61 changes: 61 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env python
import os
import re

from setuptools import setup, find_packages


ROOT = os.path.dirname(__file__)
VERSION_RE = re.compile(r'''__version__ = ['"]([0-9.]+)['"]''')


requires = [
]


def get_version():
init = open(os.path.join(ROOT, 'tiepy', '__init__.py')).read()
return VERSION_RE.search(init).group(1)


setup(
name='tiepy',
version=get_version(),
description='Build time checker for maintainable, professional Python',
long_description=open('README.md').read(),
long_description_content_type="text/markdown",
author='Mitchell Grice',
url='https://github.com/gricey432/tiepy',
packages=find_packages(exclude=['test*']),
include_package_data=True,
install_requires=[
"click>=6.5",
"dataclasses>=0.6; python_version < '3.7'",
],
extras_require={
"test": [
"pytest",
]
},
entry_points={
'console_scripts': [
'tiepy = tiepy.cli:main'
]
},
python_requires='>=3.6',
license="Apache License 2.0",
classifiers=[
"Development Status :: 2 - Pre-Alpha",
"Environment :: Console",
"Intended Audience :: Developers",
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3 :: Only",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Software Development :: Quality Assurance",
],
)
Empty file added test/__init__.py
Empty file.
Empty file.
12 changes: 12 additions & 0 deletions test/_testable_modules/overrides_1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from tiepy import overrides, final


class BaseClass:
def my_method(self):
pass


class ExtensionClass(BaseClass):
@overrides
def my_method(self):
pass
Empty file added test/conftest.py
Empty file.
11 changes: 11 additions & 0 deletions test/test_overrides.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import pytest
import runpy
import os

from tiepy.check.overrides import OverridesChecker


def test_overrides():
module = runpy.run_path(os.path.join(os.path.dirname(__file__), "_testable_modules", "overrides_1.py"))
issues = OverridesChecker().check_module(module)
assert len(issues) == 0
6 changes: 6 additions & 0 deletions tiepy/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""
Tiepy
"""
__version__ = "0.0.1"

from tiepy.overrides import overrides, final
20 changes: 20 additions & 0 deletions tiepy/check/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from abc import ABC, abstractmethod
from typing import List, Dict, Any
from dataclasses import dataclass


@dataclass
class Issue:
"""A finding with a module"""
filename: str
line_no: int
message: str

def to_print_str(self) -> str:
return f"{self.filename}:{self.line_no} {self.message}"


class Checker(ABC):
@abstractmethod
def check_module(self, module: Dict[str, Any]) -> List[Issue]:
pass
60 changes: 60 additions & 0 deletions tiepy/check/overrides.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from types import ModuleType, FunctionType
from typing import List, Dict, Any
import inspect

from tiepy.check import Issue, Checker
from tiepy.overrides import overrides, final


class OverridesChecker(Checker):
"""
Checks for issues relating to `overrides` and `final` decorators
"""

@overrides
def check_module(self, module: Dict[str, Any]) -> List[Issue]:
issues: List[Issue] = []
for module_member_name, module_member in module.items():
if inspect.isclass(module_member):
for class_member_name, class_member in inspect.getmembers(module_member):
if inspect.isfunction(class_member):
issues.extend(self.check_function(module_member, class_member, class_member_name))
return issues

def check_function(self, cls: type, function: FunctionType, name: str) -> List[Issue]:
decorated_overrides = getattr(function, "__tiepy_overrides", False)
decorated_not_overrides = getattr(function, "__tiepy_not_overrides", False)
_, lineno = inspect.getsourcelines(function)
issues: List[Issue] = []

if decorated_overrides and decorated_not_overrides:
issues.append(Issue(
filename=inspect.getfile(function),
line_no=lineno,
message="@overrides and @not_overrides can't both be on the same function",
))

found_overridden_parent = False
for parent_cls in cls.mro()[1:]: # Skip current class
if hasattr(parent_cls, name):
found_overridden_parent = True
if decorated_not_overrides:
issues.append(Issue(
filename=inspect.getfile(function),
line_no=lineno,
message=f"@no_overrides function overrides parent {parent_cls.__name__}",
))
if getattr(getattr(parent_cls, name), "__tiepy_final", False):
issues.append(Issue(
filename=inspect.getfile(function),
line_no=lineno,
message=f"@final function overrides parent {parent_cls.__name__}",
))
if decorated_overrides and not found_overridden_parent:
issues.append(Issue(
filename=inspect.getfile(function),
line_no=lineno,
message=f"@overrides function doesn't override parent",
))

return issues
57 changes: 57 additions & 0 deletions tiepy/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import sys
import logging
import re
import importlib.util
import runpy
from typing import List
from pathlib import Path

import click

from tiepy import __version__
from tiepy.check import Issue
from tiepy.check.overrides import OverridesChecker


_s3_path_re = re.compile(r"s3://(.+?)/(.+)?$")


@click.command()
@click.argument('target', type=click.Path(exists=True))
@click.option('-v', '--verbose', is_flag=True)
def tiepy(
target: str,
verbose: bool,
):
target = Path(target).absolute()
click.echo(click.style("TiePy", fg="cyan", bold=True) + f" {__version__} checking {target}")

if target.is_file():
paths = [target]
else:
paths = target.rglob("*.py")

issues: List[Issue] = []

# Overrides
overrides_checker = OverridesChecker()
for path in paths:
if verbose:
click.echo(f"Checking {path}")
module = runpy.run_path(str(path))
issues.extend(overrides_checker.check_module(module))

# Results
issues.sort(key=lambda x: (x.filename, x.line_no))
for issue in issues:
click.echo(issue.to_print_str())
click.echo(f"Found {len(issues)} issues")


def main():
logging.basicConfig(level=logging.INFO)
tiepy(sys.argv[1:])


if __name__ == "__main__":
main()
Loading

0 comments on commit 49f3f97

Please sign in to comment.