diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..7247c62 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,99 @@ +name: lint + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + pre-commit: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up python 3.8 + uses: actions/setup-python@v4 + with: + python-version: 3.8 + cache: pip + cache-dependency-path: | + setup.py + + - name: Install dependencies + run: python -m pip install -e ".[dev]" + + - name: Run pre-commit + uses: pre-commit/action@v3.0.0 + + pyright: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10"] + fail-fast: false + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: pip + cache-dependency-path: | + setup.py + + # all extras are installed to test + - name: Install dependencies + run: python -m pip install -e ".[dev]" -e ".[docs]" + + - name: Set up pyright + run: echo "PYRIGHT_VERSION=$(python -c 'import pyright; print(pyright.__pyright_version__)')" >> $GITHUB_ENV + + - name: Run pyright (Linux) + uses: jakebailey/pyright-action@v1.3.0 + with: + version: ${{ env.PYRIGHT_VERSION }} + python-version: ${{ matrix.python-version }} + python-platform: Linux + # only add comments for 3.8 + no-comments: ${{ matrix.python-version != '3.8' }} + warnings: true + + - name: Run pyright (Windows) + uses: jakebailey/pyright-action@v1.3.0 + # run anyway + if: success() || failure() + with: + version: ${{ env.PYRIGHT_VERSION }} + python-version: ${{ matrix.python-version }} + python-platform: Windows + # only add comments for one platform + no-comments: true + warnings: true + + slotscheck: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up python 3.8 + uses: actions/setup-python@v4 + with: + python-version: 3.8 + cache: pip + cache-dependency-path: | + setup.py + + - name: Install dependencies + run: python -m pip install -e ".[dev]" + + - name: Run slotscheck + run: task slotscheck diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-test.yml similarity index 89% rename from .github/workflows/python-app.yml rename to .github/workflows/python-test.yml index 6203503..72f18e1 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-test.yml @@ -1,7 +1,7 @@ # This workflow will install Python dependencies, run tests and lint with a single version of Python # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions -name: Python application +name: Test application on: workflow_dispatch: @@ -27,9 +27,6 @@ jobs: python -m pip install --upgrade pip pip install flake8==3.8.4 pytest tox if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Lint with flake8 - run: | - python setup.py lint - name: Test with pytest run: | tox diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..d598acd --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,35 @@ +## Pre-commit setup + +ci: + autofix_commit_msg: | + style: auto fixes from pre-commit hooks + +repos: + - repo: https://github.com/psf/black + rev: 22.3.0 + hooks: + - id: black + name: Running black in all files. + + - repo: https://github.com/pycqa/isort + rev: 5.10.1 + hooks: + - id: isort + args: ["--profile", "black", "--extend-skip", "table2ascii"] + name: Running isort in all files. + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.2.0 + hooks: + - id: check-ast + name: Check if python files are valid syntax for the ast parser + - id: check-case-conflict + name: Check for case conflict on file names for case insensitive systems. + - id: check-merge-conflict + name: Check for merge conflict syntax. + - id: check-toml + name: Check TOML files for valid syntax. + - id: check-yaml + name: Check YAML files for valid syntax. + - id: debug-statements + name: Check for debug statements. \ No newline at end of file diff --git a/README.md b/README.md index 45cd175..5a1da1d 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Documentation and examples are available at [table2ascii.rtfd.io](https://table2 ## 📥 Installation -``pip install -U table2ascii`` +`pip install -U table2ascii` **Requirements:** `Python 3.7+` @@ -178,16 +178,12 @@ All parameters are optional. ## 🧰 Development -### Running tests +Install development dependencies with `pip install -e .[dev]` -1. Install `tox` with the command ``pip install -U tox`` +### Running tests -2. Run tests with the command ``tox`` +Run tests with the command `tox` ### Linting -Run the following command to lint with flake8 - -``python setup.py lint`` - -(Note: The exact command may vary depending on your Python version and environment) +Run `task lint` to lint with black, isort, and pre-commit hooks. diff --git a/docs/source/_templates/__init__.py b/docs/source/_templates/__init__.py index 7e97404..aacbc5b 100644 --- a/docs/source/_templates/__init__.py +++ b/docs/source/_templates/__init__.py @@ -12,7 +12,6 @@ from sphinx.locale import _ from sphinx.util.logging import getLogger - __version__ = "1.0.0" __version_full__ = __version__ @@ -38,9 +37,7 @@ def config_initiated(app, config): # See http://www.sphinx-doc.org/en/stable/theming.html#distribute-your-theme-as-a-python-package def setup(app): if python_version[0] < 3: - logger.warning( - "Python 2 is deprecated with sphinx_rtd_theme, update to Python 3" - ) + logger.warning("Python 2 is deprecated with sphinx_rtd_theme, update to Python 3") app.require_sphinx("1.6") if sphinx_version <= (2, 0, 0): logger.warning( diff --git a/docs/source/generate_style_list.py b/docs/source/generate_style_list.py index 0d1d59d..26efe56 100644 --- a/docs/source/generate_style_list.py +++ b/docs/source/generate_style_list.py @@ -1,4 +1,5 @@ import os + __import__("sys").path.append(os.path.join(os.path.dirname(__file__), "..", "..")) from table2ascii import PresetStyle, table2ascii diff --git a/pyproject.toml b/pyproject.toml index 5358253..e529124 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,63 @@ requires = [ ] build-backend = "setuptools.build_meta" + [project] name = "table2ascii" authors = [{name = "Jonah Lawrence", email = "jonah@freshidea.com"}] -dynamic = ["version", "description"] \ No newline at end of file +dynamic = ["version", "description"] + + +[tool.black] +line-length = 100 +target-version = ["py38", "py39", "py310"] + + +[tool.isort] +profile = "black" +py_version = 38 +line_length = 100 +combine_as_imports = true +filter_files = true + + +[tool.taskipy.tasks] +black = { cmd = "task lint black", help = "Run black" } +docs = { cmd = "cd docs && sphinx-autobuild source _build/html --ignore _build --watch ../table2ascii --port 8888", help = "Build the documentation on an autoreloading server."} +isort = { cmd = "task lint isort", help = "Run isort" } +lint = { cmd = "pre-commit run --all-files", help = "Check all files for linting errors" } +precommit = { cmd = "pre-commit install --install-hooks", help = "Install the precommit hook" } +pyright = { cmd = "dotenv -f task.env run -- pyright", help = "Run pyright" } +slotscheck = { cmd = "python -m slotscheck --verbose -m table2ascii", help = "Run slotscheck" } + + +[tool.slotscheck] +strict-imports = true +require-superclass = true +require-subclass = false + + +[tool.pyright] +typeCheckingMode = "basic" +include = [ + "table2ascii", + "*.py", +] +pythonVersion = "3.7" + +# https://github.com/microsoft/pyright/blob/main/docs/configuration.md +reportInvalidStringEscapeSequence = false +reportPropertyTypeMismatch = true +reportDuplicateImport = true +reportUntypedFunctionDecorator = true +reportUntypedClassDecorator = true +reportUntypedBaseClass = true +reportUntypedNamedTuple = true +reportUnknownLambdaType = true +reportInvalidTypeVarUse = true +reportUnnecessaryCast = true +reportSelfClsParameterName = true +reportUnsupportedDunderAll = true +reportUnusedVariable = true +reportUnnecessaryComparison = true +reportUnnecessaryTypeIgnoreComment = true diff --git a/setup.py b/setup.py index 50e4c48..66a8e5d 100644 --- a/setup.py +++ b/setup.py @@ -1,53 +1,18 @@ # /usr/bin/env python import os import re + from setuptools import setup from setuptools.command.test import test as Command -class LintCommand(Command): - """ - A copy of flake8's Flake8Command - """ - - description = "Run flake8 on modules registered in setuptools" - user_options = [] - - def initialize_options(self): - # must override - pass - - def finalize_options(self): - # must override - pass - - def distribution_files(self): - if self.distribution.packages: - for package in self.distribution.packages: - yield package.replace(".", os.path.sep) - - if self.distribution.py_modules: - for filename in self.distribution.py_modules: - yield "%s.py" % filename - - def run(self): - from flake8.api.legacy import get_style_guide - - flake8_style = get_style_guide(config_file="setup.cfg") - paths = self.distribution_files() - report = flake8_style.check_files(paths) - raise SystemExit(report.total_errors > 0) - - def version(): version = "" with open("table2ascii/__init__.py") as f: - version = re.search( - r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', f.read(), re.MULTILINE - ).group(1) + version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', f.read(), re.MULTILINE) if not version: raise RuntimeError("version is not set") - return version + return version.group(1) def long_description(): @@ -68,12 +33,21 @@ def requirements(): extras_require = { "docs": [ - "sphinx>=4.4.0,<5", - "enum-tools>=0.9.0.post1,<1", - "sphinx-toolbox>=2.18.0,<3", - "sphinxcontrib_trio>=1.1.2,<2", - "sphinx-rtd-theme>=1.0.0,<2", - "sphinxext-opengraph>=0.6.2,<1", + "sphinx", + "enum-tools", + "sphinx-toolbox", + "sphinxcontrib_trio", + "sphinx-rtd-theme", + "sphinxext-opengraph", + "sphinx-autobuild", + ], + "dev": [ + "pre-commit==2.18.1", + "taskipy==1.10.1", + "slotscheck==0.14.0", + "python-dotenv==0.20.0", + "pyright==1.1.244", + "tox==3.24.5", ], } @@ -109,7 +83,4 @@ def requirements(): tests_require=[ "pytest>=6.2,<7", ], - cmdclass={ - "lint": LintCommand, - }, ) diff --git a/table2ascii/table_to_ascii.py b/table2ascii/table_to_ascii.py index e2673a0..e48f07a 100644 --- a/table2ascii/table_to_ascii.py +++ b/table2ascii/table_to_ascii.py @@ -31,9 +31,7 @@ def __init__(self, header: List, body: List[List], footer: List, options: Option # check if footer has a different number of columns if footer and len(footer) != self.__columns: - raise ValueError( - "Footer must have the same number of columns as the other rows" - ) + raise ValueError("Footer must have the same number of columns as the other rows") # check if any rows in body have a different number of columns if body and any(len(row) != self.__columns for row in body): raise ValueError( @@ -45,9 +43,7 @@ def __init__(self, header: List, body: List[List], footer: List, options: Option if options.column_widths: # check that the right number of columns were specified if len(options.column_widths) != self.__columns: - raise ValueError( - "Length of `column_widths` list must equal the number of columns" - ) + raise ValueError("Length of `column_widths` list must equal the number of columns") # check that each column is at least as large as the minimum size for i in range(len(options.column_widths)): option = options.column_widths[i] @@ -62,9 +58,7 @@ def __init__(self, header: List, body: List[List], footer: List, options: Option # check if alignments specified have a different number of columns if options.alignments and len(options.alignments) != self.__columns: - raise ValueError( - "Length of `alignments` list must equal the number of columns" - ) + raise ValueError("Length of `alignments` list must equal the number of columns") def __count_columns(self) -> int: """ @@ -101,9 +95,7 @@ def longest_line(text: str) -> int: # number of characters in column of i of header, each body row, and footer header_size = longest_line(str(self.__header[i])) if self.__header else 0 body_size = ( - map(lambda row, i=i: longest_line(str(row[i])), self.__body) - if self.__body - else [0] + map(lambda row, i=i: longest_line(str(row[i])), self.__body) if self.__body else [0] ) footer_size = longest_line(str(self.__footer[i])) if self.__footer else 0 # get the max and add 2 for padding each side with a space diff --git a/task.env b/task.env new file mode 100644 index 0000000..b0e60a2 --- /dev/null +++ b/task.env @@ -0,0 +1 @@ +PYRIGHT_PYTHON_IGNORE_WARNINGS=1 \ No newline at end of file diff --git a/tests/test_alignments.py b/tests/test_alignments.py index 3ffd1a9..555f0b6 100644 --- a/tests/test_alignments.py +++ b/tests/test_alignments.py @@ -1,7 +1,7 @@ -from table2ascii import alignment, table2ascii as t2a, Alignment - import pytest +from table2ascii import Alignment, alignment, table2ascii as t2a + def test_first_left_four_right(): text = t2a( @@ -72,7 +72,13 @@ def test_alignments_multiline_data(): header=["Multiline\nHeader\nCell", "G", "Two\nLines", "R", "S"], body=[[1, "Alpha\nBeta\nGamma", 3, 4, "One\nTwo"]], footer=["A", "Footer\nBreak", 1, "Second\nCell\nBroken", 3], - alignments=[Alignment.LEFT, Alignment.RIGHT, Alignment.CENTER, Alignment.LEFT, Alignment.CENTER], + alignments=[ + Alignment.LEFT, + Alignment.RIGHT, + Alignment.CENTER, + Alignment.LEFT, + Alignment.CENTER, + ], ) expected = ( "╔═══════════════════════════════════════════╗\n" diff --git a/tests/test_column_widths.py b/tests/test_column_widths.py index 5ae9ace..feecfc0 100644 --- a/tests/test_column_widths.py +++ b/tests/test_column_widths.py @@ -1,7 +1,7 @@ -from table2ascii import table2ascii as t2a - import pytest +from table2ascii import table2ascii as t2a + def test_column_widths(): text = t2a( diff --git a/tests/test_convert.py b/tests/test_convert.py index b0d71cc..cb67a03 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -1,7 +1,7 @@ -from table2ascii import alignment, table2ascii as t2a - import pytest +from table2ascii import alignment, table2ascii as t2a + def test_header_body_footer(): text = t2a( diff --git a/tests/test_styles.py b/tests/test_styles.py index 0e8a553..5c7464d 100644 --- a/tests/test_styles.py +++ b/tests/test_styles.py @@ -1,4 +1,4 @@ -from table2ascii import table2ascii as t2a, PresetStyle +from table2ascii import PresetStyle, table2ascii as t2a def test_thin():