From 0a590ba117f83585bdd563d3e077351701417574 Mon Sep 17 00:00:00 2001 From: tomas Date: Tue, 7 Nov 2023 15:10:19 -0800 Subject: [PATCH 1/4] Switch from setuptools and pip to poetry for dependency management The project's dependency management has been completely overhauled. Files associated with the old system (like .travis.yml, Pipfile, setup.cfg, and setup.py) have been deleted. A new pyproject.toml file, which is used by the poetry tool, has been added. --- .travis.yml | 32 ----- Pipfile | 16 --- Pipfile.lock | 291 ------------------------------------------- pyproject.toml | 260 ++++++++++++++++++++++++++++++++++++++ requirements.dev.txt | 6 - setup.cfg | 60 --------- setup.py | 2 - 7 files changed, 260 insertions(+), 407 deletions(-) delete mode 100644 .travis.yml delete mode 100644 Pipfile delete mode 100644 Pipfile.lock create mode 100644 pyproject.toml delete mode 100644 requirements.dev.txt delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 465e975..0000000 --- a/.travis.yml +++ /dev/null @@ -1,32 +0,0 @@ -language: python -dist: focal -sudo: required -services: -- docker -python: -- '3.6' -- '3.7' -before_install: -- sudo apt-get install -y libev4 libpcre3-dev -install: -- bash <(curl -sL get.comby.dev) -- pip install -r requirements.dev.txt -- pip install tox-travis==0.12 -# https://travis-ci.community/t/cant-deploy-to-pypi-anymore-pkg-resources-contextualversionconflict-importlib-metadata-0-18/10494/4 -- pip install keyring==21.4.0 -# see #337: flag potential twine issues before attempting to upload to PyPI -- python setup.py sdist -- python setup.py bdist_wheel -- twine check dist/* -script: -- tox -notifications: - email: false -deploy: - - provider: pypi - user: ChrisTimperley - on: - tags: true - python: 3.6 - password: - secure: DRaJkyVgjaZwu1qHMTIn5fP4uwSpd94JFHL8j5/mKzP8dVWNRHXlYVqDkoYxOx//PYSatb76thDX0jETHJ3xqrnRymBick+PN5Ak+mzJ2iP75d5ELHdSJZmNYo2UqTEip8VOlOic6722w53pO37zDqAfuk3Qs70x9nVS0SOz4KFymIfcMjrajruPL7+cSu79mlbn5xuKV9sPl+JM4UF0X/EwPd1KURjIoRXH3BBj98mfjgrx/5kjIbtuh80fPPh9JHgx2T/dMEZMMHANgtfarCXvMJ0eDv9UbWo6u/Y/Pl1/rxS51y7Bu8okudQUXc2N9CZxKJmGpZNnX0tU+vzIRXvuKV13ok1JmLdxdRl+wNOih6jZHiDjPOhyAFrMVrnK2Ho8t+nsp+y9IYLKcC/LOIbB+sE0FpJ/DWwaw2KTGfDnBafnoeb/MO80e1VOgEA+x/0CmTzqUUFpP3X6TKSUVcLAEyoLlvhu948OCuQD6MJmFK1BH261COmGscB4QwI6yWq9qIjsQjF62PApUrokWS+YN48JEIu633D8wzPObv/xaOyRlwhkscu/lSLXXX9UYzGCr56mu9hZSkUheAPN4pkE+4TeCwWGzWKATIABJR8ZEhuve0VjV7SBdWrD2mbK55KKKsB7UXzpe5niRZP6GlkjjDS20gAZdud+UT86HR0= diff --git a/Pipfile b/Pipfile deleted file mode 100644 index f4ae6c9..0000000 --- a/Pipfile +++ /dev/null @@ -1,16 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -comby = {editable = true, path = "."} - -[dev-packages] -tox = "*" -mypy = "*" -flake8 = "*" -pytest = "*" - -[requires] -python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index 97f1cc8..0000000 --- a/Pipfile.lock +++ /dev/null @@ -1,291 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "17c449f2f1bfe247bbd5ebfe5f88ec751650c139694292ae404048b401e9a334" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.9" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "attrs": { - "hashes": [ - "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6", - "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c" - ], - "markers": "python_version >= '3.5'", - "version": "==22.1.0" - }, - "certifi": { - "hashes": [ - "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14", - "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382" - ], - "markers": "python_full_version >= '3.6.0'", - "version": "==2022.9.24" - }, - "charset-normalizer": { - "hashes": [ - "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", - "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" - ], - "markers": "python_full_version >= '3.6.0'", - "version": "==2.1.1" - }, - "comby": { - "editable": true, - "path": "." - }, - "idna": { - "hashes": [ - "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", - "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" - ], - "markers": "python_version >= '3.5'", - "version": "==3.4" - }, - "loguru": { - "hashes": [ - "sha256:066bd06758d0a513e9836fd9c6b5a75bfb3fd36841f4b996bc60b547a309d41c", - "sha256:4e2414d534a2ab57573365b3e6d0234dfb1d84b68b7f3b948e6fb743860a77c3" - ], - "markers": "python_version >= '3.5'", - "version": "==0.6.0" - }, - "requests": { - "hashes": [ - "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", - "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" - ], - "markers": "python_version >= '3.7' and python_version < '4'", - "version": "==2.28.1" - }, - "typing": { - "hashes": [ - "sha256:1187fb9c82fd670d10aa07bbb6cfcfe4bdda42d6fab8d5134f04e8c4d0b71cc9", - "sha256:283d868f5071ab9ad873e5e52268d611e851c870a2ba354193026f2dfb29d8b5" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==3.7.4.3" - }, - "urllib3": { - "hashes": [ - "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc", - "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.26.13" - } - }, - "develop": { - "attrs": { - "hashes": [ - "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6", - "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c" - ], - "markers": "python_version >= '3.5'", - "version": "==22.1.0" - }, - "distlib": { - "hashes": [ - "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46", - "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e" - ], - "version": "==0.3.6" - }, - "exceptiongroup": { - "hashes": [ - "sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828", - "sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec" - ], - "markers": "python_version < '3.11'", - "version": "==1.0.4" - }, - "filelock": { - "hashes": [ - "sha256:7565f628ea56bfcd8e54e42bdc55da899c85c1abfe1b5bcfd147e9188cebb3b2", - "sha256:8df285554452285f79c035efb0c861eb33a4bcfa5b7a137016e32e6a90f9792c" - ], - "markers": "python_version >= '3.7'", - "version": "==3.8.2" - }, - "flake8": { - "hashes": [ - "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7", - "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181" - ], - "index": "pypi", - "version": "==6.0.0" - }, - "iniconfig": { - "hashes": [ - "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", - "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" - ], - "version": "==1.1.1" - }, - "mccabe": { - "hashes": [ - "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", - "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" - ], - "markers": "python_version >= '3.6'", - "version": "==0.7.0" - }, - "mypy": { - "hashes": [ - "sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d", - "sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6", - "sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf", - "sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f", - "sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813", - "sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33", - "sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad", - "sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05", - "sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297", - "sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06", - "sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd", - "sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243", - "sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305", - "sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476", - "sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711", - "sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70", - "sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5", - "sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461", - "sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab", - "sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c", - "sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d", - "sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135", - "sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93", - "sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648", - "sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a", - "sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb", - "sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3", - "sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372", - "sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb", - "sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef" - ], - "index": "pypi", - "version": "==0.991" - }, - "mypy-extensions": { - "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" - ], - "version": "==0.4.3" - }, - "packaging": { - "hashes": [ - "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", - "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" - ], - "markers": "python_version >= '3.6'", - "version": "==21.3" - }, - "platformdirs": { - "hashes": [ - "sha256:1a89a12377800c81983db6be069ec068eee989748799b946cce2a6e80dcc54ca", - "sha256:b46ffafa316e6b83b47489d240ce17173f123a9b9c83282141c3daf26ad9ac2e" - ], - "markers": "python_version >= '3.7'", - "version": "==2.6.0" - }, - "pluggy": { - "hashes": [ - "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", - "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" - ], - "markers": "python_version >= '3.6'", - "version": "==1.0.0" - }, - "py": { - "hashes": [ - "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", - "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.11.0" - }, - "pycodestyle": { - "hashes": [ - "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053", - "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610" - ], - "markers": "python_version >= '3.6'", - "version": "==2.10.0" - }, - "pyflakes": { - "hashes": [ - "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf", - "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd" - ], - "markers": "python_version >= '3.6'", - "version": "==3.0.1" - }, - "pyparsing": { - "hashes": [ - "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", - "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" - ], - "markers": "python_full_version >= '3.6.8'", - "version": "==3.0.9" - }, - "pytest": { - "hashes": [ - "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71", - "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59" - ], - "index": "pypi", - "version": "==7.2.0" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "tomli": { - "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" - ], - "markers": "python_version < '3.11'", - "version": "==2.0.1" - }, - "tox": { - "hashes": [ - "sha256:b2a920e35a668cc06942ffd1cf3a4fb221a4d909ca72191fb6d84b0b18a7be04", - "sha256:f52ca66eae115fcfef0e77ef81fd107133d295c97c52df337adedb8dfac6ab84" - ], - "index": "pypi", - "version": "==3.27.1" - }, - "typing-extensions": { - "hashes": [ - "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", - "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" - ], - "markers": "python_version >= '3.7'", - "version": "==4.4.0" - }, - "virtualenv": { - "hashes": [ - "sha256:ce3b1684d6e1a20a3e5ed36795a97dfc6af29bc3970ca8dab93e11ac6094b3c4", - "sha256:f8b927684efc6f1cc206c9db297a570ab9ad0e51c16fa9e45487d36d1905c058" - ], - "markers": "python_version >= '3.6'", - "version": "==20.17.1" - } - } -} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..946a428 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,260 @@ +[tool.poetry] +name = "comby" +version = "0.0.4" +description = "Lightweight language-independent syntax rewriting." +authors = ["Christopher Timperley "] +license = "MIT" +readme = "README.rst" +repository = "https://github.com/ChrisTimperley/comby-python" +classifiers = [ + "Natural Language :: English", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Typing :: Typed", + # Include this classifier to prevent accidently publishing private code to PyPI. + # https://pypi.org/classifiers/ + "Private :: Do Not Upload", +] + +[tool.poetry.dependencies] +python = "^3.9, <3.13" +loguru = "^0.7.2" +attrs = "^23.1.0" + +[tool.poetry.group.nox.dependencies] +nox-poetry = ">=1.0.3" + +[tool.poetry.group.test.dependencies] +pytest = "*" +pytest-cov = "*" +pytest-randomly = "*" + +[tool.poetry.group.type_check.dependencies] +mypy = "*" +typeguard = ">=4.1.5" +pydantic = ">=2.4.2" +[tool.poetry.group.lint.dependencies] +ruff = "*" +Flake8-pyproject = "*" +flake8 = "*" +pylint = "*" + +dlint = ">=0.14.1" +flake8-aaa = ">=0.17.0" +flake8-absolute-import = ">=1.0.0.2" +flake8-annotations = ">=3.0.1" +flake8-annotations-complexity = ">=0.0.8" +flake8-annotations-coverage = ">=0.0.6" +flake8-black = ">=0.3.6" +flake8-broken-line = ">=1.0.0" +flake8-class-attributes-order = ">=0.1.3" +flake8-cognitive-complexity = ">=0.1.0" +flake8-cohesion = ">=1.0.1" +flake8-comprehensions = ">=3.14.0" +flake8-deprecated = ">=2.1.0" +flake8-docstrings = ">=1.7.0" +flake8-dunder-all = ">=0.3.0" +flake8-eradicate = ">=1.5.0" +flake8-expression-complexity = ">=0.0.11" +flake8-fixme = ">=1.1.1" +flake8-functions = ">=0.0.8" +flake8-functions-names = ">=0.4.0" +flake8-future-annotations = ">=1.1.0" +flake8-markdown = ">=0.5.0" +flake8-mutable = ">=1.2.0" +flake8-newspaper-style = ">=0.4.3" +flake8-no-implicit-concat = ">=0.3.4" +flake8-print = ">=5.0.0" +flake8-printf-formatting = ">=1.1.2" +flake8-pylint = ">=0.2.1" +flake8-pytest-style = ">=1.7.2" +flake8-quotes = ">=3.3.2" +flake8-rst-docstrings = ">=0.3.0" +flake8-scrapy = ">=0.0.1" +flake8-secure-coding-standard = ">=1.4.0" +flake8-string-format = ">=0.3.0" +flake8-todos = ">=0.3.0" +flake8-tuple = ">=0.4.1" +flake8-typing-imports = ">=1.15.0" +flake8-unused-arguments = ">=0.0.13" +flake8-use-fstring = ">=1.4" +flake8-use-pathlib = ">=0.3.0" +flake8-variables-names = ">=0.0.6" +flake8-warnings = ">=0.4.1" +mccabe = ">=0.7.0" +tryceratops = ">=2.3.2" +types-flake8-2020 = ">=1.8.0.1" +types-flake8-builtins = ">=2.1.0.3" + +python-typing-update = ">=0.6.0" + + +[tool.poetry.group.fmt.dependencies] +black = "*" +docformatter = ">=1.7.5" +pyment = { git = "https://github.com/dadadel/pyment.git" } + +[tool.poetry.group.docs.dependencies] +mkdocs-material = "*" +mkdocs-htmlproofer-plugin = "*" +mkdocstrings = { version = "*", extras = ["python"] } +mkdocs-gen-files = "*" +mkdocs-literate-nav = "*" + +[tool.mypy] +ignore_missing_imports = true +strict = true + +[tool.ruff] +line-length = 100 +target-version = "py38" +preview = true +extend-select = [ + "F", # Pyflakes + "E", # pycodestyle + "W", # Warning + "E", # Error + "C90", # mccabe + "I", # isort + "N", # pep8-naming + "D", # pydocstyle + "UP", # pyupgrade + "YTT", # flake8-2020 + "ANN", # flake8-annotations + "ASYNC", # flake8-async + "S", # flake8-bandit + "BLE", # flake8-blind-except + "FBT", # flake8-boolean-trap + "B", # flake8-bugbear + "A", # flake8-builtins + # "COM", # flake8-commas + # "CPY", # flake8-copyright + "C4", # flake8-comprehensions + "DTZ", # flake8-datetimez + "T10", # flake8-debugger + "DJ", # flake8-django + "EM", # flake8-errmsg + "EXE", # flake8-executable + "FA", # flake8-future-annotations + "ISC", # flake8-implicit-str-concat + "ICN", # flake8-import-conventions + "G", # flake8-logging-format + "INP", # flake8-no-pep420 + "PIE", # flake8-pie + "T20", # flake8-print + "PYI", # flake8-pyi + "PT", # flake8-pytest-style + "Q", # flake8-quotes + "RSE", # flake8-raise + "RET", # flake8-return + "SLF", # flake8-self + "SLOT", # flake8-slots + "SIM", # flake8-simplify + "TID", # flake8-tidy-imports + "TCH", # flake8-type-checking + "INT", # flake8-gettext + "ARG", # flake8-unused-arguments + "PTH", # flake8-use-pathlib + "TD", # flake8-todos + "FIX", #flake8-fixme + 'ERA', # eradicate + 'PD', # pandas-vet + 'PGH', # pygrep-hooks + 'PL', # Pylint + 'TRY', # tryceratops + # 'FLY', # flynt + 'NPY', # NumPy-specific rules + 'AIR', # Airflow + 'PERF', # Perflint + 'FURB', # refurb + 'LOG', #flake8-logging + 'RUF', # Ruff-specific rules +] +extend-ignore = [ + "RUF005", + "D200", # One-line docstring should fit on one line with quotes + "D212", # Multi-line docstring summary should start at the first line + "D410", # Missing blank line after section + "D411", # Missing blank line before section + "Q000", # Double quotes found but single quotes preferred +] +src = ["src"] + +[tool.ruff.flake8-tidy-imports] +ban-relative-imports = "all" + +[tool.ruff.flake8-bugbear] +extend-immutable-calls = ["typer.Argument"] + +[tool.ruff.isort] +force-sort-within-sections = false +split-on-trailing-comma = true + +[tool.ruff.pydocstyle] +convention = "google" + + +[tool.ruff.per-file-ignores] +"docs/gen_ref_pages.py" = ["INP001", "PGH003"] +"src/comby/core.py" = ["A003"] +"src/comby/binary.py" = ["PLR0913", "S602"] +"src/comby/interface.py" = ["PLR0913"] +"**/test_*.py" = ["S101", "E501"] +"noxfile.py" = ["PIE800"] +[tool.flake8] +max-line-length = 100 +docstring-convention = "google" + +extend-exclude = ["venv", ".venv", "build"] +skip-overridden-methods = true +unused-arguments-ignore-abstract-functions = true +unused-arguments-ignore-overload-functions = true +unused-arguments-ignore-override-functions = true +unused-arguments-ignore-stub-functions = true +unused-arguments-ignore-variadic-names = true +unused-arguments-ignore-lambdas = true + +ignore = [ + "D200", # One-line docstring should fit on one line with quotes + "D212", # Multi-line docstring summary should start at the first line + "D410", # Missing blank line after section + "D411", # Missing blank line before section + "FNE002", # The method has a @property decorator, but has a verb in it's name (version + "RST203", # Definition list ends without a blank line; unexpected unindent. + "RST213", # Inline emphasis start-string without end-string. + "TYP001", # guard import by `if False: # TYPE_CHECKING`: override + "PLW301", # Unnecessary ellipsis constant (unnecessary-ellipsis) + "Q000", # Double quotes found but single quotes preferred +] + +per-file-ignores = [ + "test_*.py: D103 DALL000 E501 PLC301 PLC116 PLW621 S101 SCS108", + "core.py: PLR903 VNE003", + "binary.py: CFQ002 PLR913 DUO116 SCS103 PLR914 CCR001 H601", + "interface.py: CFQ002 PLR913", + "noxfile.py: PIE800", +] +[tool.pylint] +max-line-length = 100 + +[tool.black] +line-length = 100 +target-version = ["py39", "py310", "py311", "py312"] + +[tool.pytest.ini_options] +addopts = ["--strict-config", "--strict-markers"] +xfail_strict = true +filterwarnings = [ + "error", +] + +[tool.coverage.run] +branch = true diff --git a/requirements.dev.txt b/requirements.dev.txt deleted file mode 100644 index 8375f25..0000000 --- a/requirements.dev.txt +++ /dev/null @@ -1,6 +0,0 @@ -flake8==3.8.4 -mypy==0.790 -pytest==6.1.2 -ptpython==3.0.7 -tox==3.20.1 -twine==3.1.1 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 3ef259b..0000000 --- a/setup.cfg +++ /dev/null @@ -1,60 +0,0 @@ -[metadata] -name = comby -version = 0.0.4 -author = Christopher Timperley -author-email = christimperley@googlemail.com -url = https://github.com/ChrisTimperley/comby-python -description = Lightweight language-independent syntax rewriting. -long-description = file: README.rst, LICENSE -long-descripion-content-type = text/reStructuredText -keywords = rewrite, syntax, comby, transformation -license = MIT -classifiers = - Natural Language :: English - Intended Audience :: Developers - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - -[options] -python_requires = >= 3.6 -install_requires = - attrs >= 19.2.0 - loguru >= 0.4 - requests ~= 2.0 - typing >= 0.4 -package_dir = - =src -packages = find: - -[options.packages.find] -where = src - -[flake8] -per-file-ignores = - src/comby/__init__.py:F401 - -[mypy] -ignore_missing_imports = True - -[aliases] -test = pytest - -[tool:pytest] -addopts = -rx -v - -[tox:tox] -envlist = py37, py38 - -[testenv] -deps = - mypy - flake8 - pytest -commands = - mypy src - flake8 src - pytest diff --git a/setup.py b/setup.py deleted file mode 100644 index cb89f27..0000000 --- a/setup.py +++ /dev/null @@ -1,2 +0,0 @@ -# -*- coding: utf-8 -*- -import setuptools; setuptools.setup() From a08483e7360e212f5e2cfd78f21c4aa3cbd71db9 Mon Sep 17 00:00:00 2001 From: tomas Date: Tue, 7 Nov 2023 15:11:51 -0800 Subject: [PATCH 2/4] Update .gitignore file to support various Python tools and configurations The .gitignore file has been greatly expanded to include files and directories created by a variety of tools and configurations used in Python development. It now covers everything from byte-compiled files, distribution packaging, unit test reports, to dependency lock files and environment settings. --- .gitignore | 193 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 182 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index f1745af..532dbb3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,187 @@ -build +# Byte-compiled / optimized / DLL files +__pycache__/ +__pycache__ +*.py[cod] +*$py.class -.tox -*.egg-info +# C extensions +*.so -.mypy_cache +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +#lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST -.pytest_cache -env +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec -*.pyc -*.pyo +# Installer logs +pip-log.txt +pip-delete-this-directory.txt -*.tmp -*.swp -*.swo +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +.venv*/ +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# ruff +.ruff_cache/ + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# coverage +coverage/ + +# Project root +project_root_directory/ + +# Monkeytype +monkeytype.sqlite3 + +# pycharm +.idea/ +qodana.yaml + +# Trash +*#*.* + +# Typings +typings/ + +# database files +*.db +*.sqlite3 From 256e793c7def434b7ee23ea42be80390f56a0fe9 Mon Sep 17 00:00:00 2001 From: tomas Date: Tue, 7 Nov 2023 15:15:44 -0800 Subject: [PATCH 3/4] Improve module typing and add match-newline-at-toplevel feature --- src/comby/__init__.py | 28 +++- src/comby/binary.py | 354 +++++++++++++++++++++++++++------------- src/comby/core.py | 351 +++++++++++++++++++++++++++------------ src/comby/exceptions.py | 34 ++-- src/comby/interface.py | 156 +++++++++++------- src/comby/py.typed | 0 test/test_binary.py | 81 --------- tests/__init__.py | 1 + tests/test_binary.py | 227 ++++++++++++++++++++++++++ 9 files changed, 853 insertions(+), 379 deletions(-) create mode 100644 src/comby/py.typed delete mode 100644 test/test_binary.py create mode 100644 tests/__init__.py create mode 100644 tests/test_binary.py diff --git a/src/comby/__init__.py b/src/comby/__init__.py index e9e88b1..a6cf228 100644 --- a/src/comby/__init__.py +++ b/src/comby/__init__.py @@ -1,16 +1,28 @@ -# -*- coding: utf-8 -*- """ -This package provides a set of Python bindings for Comby, a general purpose -tool for matching and rewriting code in arbitrary languages. +Python bindings for Comby. + +This package provides a set of Python bindings for Comby, a general purpose tool for +matching and rewriting code in arbitrary languages. + """ from loguru import logger as _logger -from . import exceptions -from .core import Location, LocationRange, BoundTerm, Environment, Match -from .binary import CombyBinary +from comby import exceptions +from comby.binary import CombyBinary +from comby.core import BoundTerm, Environment, Location, LocationRange, Match Comby = CombyBinary -__version__ = '0.0.2' +__version__ = "0.0.2" + +_logger.disable("comby") -_logger.disable('comby') +__all__ = [ + "exceptions", + "Location", + "LocationRange", + "BoundTerm", + "Environment", + "Match", + "CombyBinary", +] diff --git a/src/comby/binary.py b/src/comby/binary.py index 6147058..09979ce 100644 --- a/src/comby/binary.py +++ b/src/comby/binary.py @@ -1,126 +1,232 @@ -# -*- coding: utf-8 -*- -""" -This module implements an binary-based interface for communicating with Comby. -""" -__all__ = ('CombyBinary',) +"""This module provides access to the Comby api.""" +from __future__ import annotations + +__all__ = ("CombyBinary",) -from typing import Iterator, Optional, Dict import json import os -import subprocess import shlex +import subprocess +from typing import TYPE_CHECKING, override -from loguru import logger import attr +from attrs import field +from loguru import logger +from typeguard import typechecked + +from comby.core import ConfigJson, ConfigMatch, Environment, LocationRange, Match +from comby.exceptions import CombyBinaryError +from comby.interface import CombyInterface + +if TYPE_CHECKING: + from typing import Iterator, Sequence -from .core import Match -from .exceptions import CombyBinaryError -from .interface import CombyInterface _LINE_SEPARATOR = os.linesep _LINE_SEPARATOR_LENGTH = len(_LINE_SEPARATOR) -@attr.s(frozen=True, slots=True) +@typechecked +@attr.s(auto_attribs=True, frozen=True, slots=True) class CombyBinary(CombyInterface): - """Provides an interface to the Comby binary. - - Attributes - ---------- - location: str - The location of the Comby binary that should be used. - language: str - The default language that should be assumed when dealing with source - text where no specific language is specified. - version: str - The version of Comby that is used by this binary. """ - location = attr.ib(type=str, default='comby') - language = attr.ib(type=str, default='.c') + Provides an interface to the Comby binary. + + location: The location of the Comby binary that should be used. + language: The default language that should be assumed when dealing with source text where no + specific language is specified. + + """ + + location: str = field(init=False, default="comby") + language: str = field(init=False, default=".c") @property - def version(self) -> str: - return self.call('-version').strip() - - def call(self, args: str, text: Optional[str] = None) -> str: - """Calls the Comby binary. - - Arguments - --------- - args: str - the arguments that should be supplied to the binary. - text: Optional[str] - the optional input text that should be supplied to the binary. - - Returns - ------- - str - the output of the execution. - - Raises - ------ - CombyBinaryError - if the binary produces a non-zero return code. + @override + def version(self: CombyInterface) -> str: """ - logger.debug(f'calling comby with args: {args}') + Retrieve the version number of the Comby binary. + + This method accesses the version information of the Comby binary and returns it as a string. + If the object is not a CombyBinary instance, it raises a TypeError. + + + :returns: The version number of the Comby binary. + + :rtype: str + :raises TypeError: If the object is not an instance of CombyBinary. + + """ + if not isinstance(self, CombyBinary): + raise TypeError + return self.call("-version").strip() + + def call(self: CombyBinary, args: str, text: str | None = None) -> str: + """ + Call the Comby binary. + + :param args: the arguments that should be supplied to the binary. + :type args: str + :param text: (Default value = None) the arguments that should be supplied to the binary. + :type text: str | None + :raises CombyBinaryError: if the binary produces a non-zero return code. + + """ + logger.debug(f"calling comby with args: {args}") input_ = None if text: - input_ = text.encode('utf8') - logger.debug(f'supplying input text: {text}') - - cmd_s = f'{self.location} {args}' - p = subprocess.run(cmd_s, - shell=True, - stderr=subprocess.PIPE, - stdout=subprocess.PIPE, - input=input_) - - err = p.stderr.decode('utf8') - out = p.stdout.decode('utf8') - logger.debug(f'stderr: {err}') - logger.debug(f'stdout: {out}') - - if p.returncode != 0: - raise CombyBinaryError(p.returncode, err) + input_ = text.encode("utf8") + logger.debug(f"supplying input text: {text}") + + cmd_s = f"{self.location} {args}" + subprocess_result = subprocess.run( + cmd_s, + shell=True, + capture_output=True, + input=input_, + check=True, + ) + + err = subprocess_result.stderr.decode("utf8") + out = subprocess_result.stdout.decode("utf8") + logger.debug(f"stderr: {err}") + logger.debug(f"stdout: {out}") + + if subprocess_result.returncode != 0: + raise CombyBinaryError(code=subprocess_result.returncode, message=err) return out - def matches(self, - source: str, - template: str, - *, - language: Optional[str] = None - ) -> Iterator[Match]: - logger.info(f"finding matches of template [{template}] " - f"in source: {source}") + @override + def matches( + self: CombyInterface, + source: str, + template: str, + *, + language: str | None = None, + ) -> Iterator[Match]: + """ + List of matches of a given template within a source text. + + :param source: the source text to search for matches. + :type source: str + :param template: the comby template to use to search for matches. + :type template: str + :param *: + :param language: (Default value = None) the language to use for the matcher. + :type language: str | None + """ + if not isinstance(self, CombyBinary): + raise TypeError + match_log: str = " ".join( + [ + "finding matches of template", + template, + " in source:", + source, + ], + ) + + logger.info(match_log) if language: logger.info(f"using language override: {language}") else: language = self.language logger.info(f"using default language: {language}") - cmd = ('-stdin', '-json-lines', '-match-only', - '-matcher', shlex.quote(language), - shlex.quote(template), 'foo') - cmd_s = ' '.join(cmd) + cmd = ( + "-stdin", + "-json-lines", + "-match-only", + "-matcher", + shlex.quote(language), + shlex.quote(template), + "foo", + ) + cmd_s: str = " ".join(cmd) + + jsn: ConfigJson | None = json.loads(self.call(cmd_s, text=source) or "null") - jsn = json.loads(self.call(cmd_s, text=source) or 'null') if jsn is not None: - jsn = jsn['matches'] - for jsn_match in jsn: - yield Match.from_dict(jsn_match) - - def rewrite(self, - source: str, - match: str, - rewrite: str, - args: Optional[Dict[str, str]] = None, - *, - diff: bool = False, - language: Optional[str] = None - ) -> str: - logger.info(f"performing rewriting of source ({source}) using match " - f"template ({match}), rewrite template ({rewrite}), " - f"and arguments ({repr(args)})") + jsn_matches: list[ConfigMatch] | None = jsn.get("matches") + if jsn_matches is None: + raise TypeError + jsn_match: ConfigMatch + for jsn_match in jsn_matches: + matched_raw: str | None = jsn_match.get("matched") + if matched_raw is None: + raise TypeError + matched: str = matched_raw + + location_raw: dict[str, dict[str, int]] | None = jsn_match.get("range") + if location_raw is None: + raise TypeError + location: LocationRange = LocationRange.from_dict(location_raw) + + environment_raw: Sequence[ + dict[str, str | dict[str, dict[str, int]]] + ] | None = jsn_match.get("environment") + if environment_raw is None: + raise TypeError + environment: Environment = Environment.from_dict( + environment_raw + ) # Assuming Environment.from_dict exists + + # Create a new dictionary with converted types + converted_jsn_match: dict[str, str | LocationRange | Environment] = { + "matched": matched, + "location": location, + "environment": environment, + } + + yield Match.from_dict(converted_jsn_match) + + @override + def rewrite( + self: CombyInterface, + source: str, + match: str, + rewrite: str, + args: dict[str, str] | None = None, + *, + diff: bool = False, + language: str | None = None, + match_newline_at_toplevel: bool = False, + ) -> str: + """ + Rewrite all matches in source with rewrite template. + + :param source: the source text to rewrite. + :type source: str + :param match: the comby template to use to search for matches. + :type match: str + :param rewrite: the comby template to use to rewrite matches. + :type rewrite: str + :param args: (Default value = None) + :type args: dict[str, str] | None + :param *: + :param diff: (Default value = False) + :type diff: bool + :param language: (Default value = None) the language to use for the matcher. + :type language: str | None + :param match_newline_at_toplevel: (Default value = False) whether to match newline + at toplevel. + :type match_newline_at_toplevel: bool + """ + if not isinstance(self, CombyBinary): + raise TypeError + rewrite_log = " ".join( + [ + "performing rewriting of source", + source, + "using match template", + match, + "rewrite template", + rewrite, + "and arguments", + f"{args!r}", + ], + ) + logger.info(rewrite_log) if language: logger.info(f"using language override: {language}") else: @@ -130,38 +236,60 @@ def rewrite(self, if args is None: args = {} if args: - raise NotImplementedError("args are not currently supported") + raise NotImplementedError - cmd = ['-stdin', shlex.quote(match), shlex.quote(rewrite)] - cmd += ['-matcher', shlex.quote(language)] - cmd += ['-diff' if diff else '-stdout'] - cmd_s = ' '.join(cmd) + cmd = ["-stdin", shlex.quote(match), shlex.quote(rewrite)] + cmd += ["-matcher", shlex.quote(language)] + cmd += ["-diff" if diff else "-stdout"] + if match_newline_at_toplevel: + cmd += ["-match-newline-at-toplevel"] + cmd_s = " ".join(cmd) return self.call(cmd_s, text=source) - def substitute(self, - template: str, - args: Dict[str, str], - *, - language: Optional[str] = None - ) -> str: - logger.info(f"performing substitution of arguments ({args}) " - f"into template ({template})") + @override + def substitute( + self: CombyInterface, + template: str, + args: dict[str, str], + *, + language: str | None = None, + ) -> str: + """ + Substitutes a set of terms into a given template. + + :param template: + :type template: str + :param args: + :type args: dict[str, str] + :param *: + :param language: (Default value = None) the language to use for the matcher. + :type language: str | None + """ + if not isinstance(self, CombyBinary): + raise TypeError + logger.info(f"performing substitution of arguments ({args}) into template ({template})") if language: logger.info(f"using language override: {language}") else: language = self.language logger.info(f"using default language: {language}") - substitutions = [{'variable': variable, 'value': value} - for (variable, value) in args.items()] + substitutions = [ + {"variable": variable, "value": value} for (variable, value) in args.items() + ] encoded_substitutions = shlex.quote(json.dumps(substitutions)) logger.debug(f"encoded substitutions: {encoded_substitutions}") - cmd = ('IGNORE_MATCHED_TEMPLATE', shlex.quote(template), - '-matcher', shlex.quote(language), - '-substitute-only', encoded_substitutions) - cmd_string = ' '.join(cmd) + cmd = ( + "IGNORE_MATCHED_TEMPLATE", + shlex.quote(template), + "-matcher", + shlex.quote(language), + "-substitute-only", + encoded_substitutions, + ) + cmd_string = " ".join(cmd) result = self.call(cmd_string) # remove any trailing line separator diff --git a/src/comby/core.py b/src/comby/core.py index 5389fa7..70c2bae 100644 --- a/src/comby/core.py +++ b/src/comby/core.py @@ -1,162 +1,309 @@ -# -*- coding: utf-8 -*- -""" -This module defines several common data structures for describing code -transformations, source locations, environments, matches, and templates. -""" -__all__ = ( - 'Location', - 'LocationRange', - 'BoundTerm', - 'Environment', - 'Match' -) - -from typing import Dict, Iterator, Any, Mapping, Sequence +"""This module provides the core data structures.""" +from __future__ import annotations + +__all__ = ("Location", "LocationRange", "BoundTerm", "Environment", "Match") + +from typing import Iterator, Mapping, Sequence, TypedDict, override import attr +from typeguard import typechecked + +# Create a new Location object. -@attr.s(frozen=True, slots=True, str=False) +@typechecked +@attr.s(auto_attribs=True, frozen=True, slots=True) class Location: """ - Represents the location of a single character within a source text by its - zero-indexed line and column numbers. + Represent a location in source code, with line, column, and byte offset. + + This is an immutable data class. + + """ - Attributes - ---------- line: int - Zero-indexed line number. col: int - Zero-indexed column number. offset: int - Zero-indexed character offset. - """ - line = attr.ib(type=int) - col = attr.ib(type=int) - offset = attr.ib(type=int) @staticmethod - def from_dict(d: Dict[str, Any]) -> 'Location': - return Location(line=d['line'], - col=d['column'], - offset=d['offset']) + def from_dict(location_dict: dict[str, int]) -> Location: + """ + Create a Location object from a dictionary. + + :param location_dict: A dictionary containing the keys "line", "column", and "offset". + :type location_dict: dict[str, int] + :returns: A Location object. + :rtype: Location + """ + return Location( + line=location_dict["line"], + col=location_dict["column"], + offset=location_dict["offset"], + ) -@attr.s(frozen=True, slots=True, str=False) +@typechecked +@attr.s(auto_attribs=True, frozen=True, slots=True, str=False) class LocationRange: - """ - Represents a contiguous range of locations within a given source text as a - (non-inclusive) range of character positions. + """Represent a range of locations in a source code.""" - Attributes - ---------- start: Location - The start of the range. stop: Location - The (non-inclusive) end of the range. - """ - start = attr.ib(type=Location) - stop = attr.ib(type=Location) @staticmethod - def from_dict(d: Dict[str, Any]) -> 'LocationRange': - return LocationRange(Location.from_dict(d['start']), - Location.from_dict(d['end'])) + def from_dict(location_range_dict: dict[str, dict[str, int]]) -> LocationRange: + """ + Create a LocationRange object from a dictionary representation. + + :param location_range_dict: + :type location_range_dict: dict[str, dict[str, int]] + :returns: The created LocationRange object. + :rtype: LocationRange + """ + return LocationRange( + start=Location.from_dict(location_range_dict["start"]), + stop=Location.from_dict( + location_range_dict["end"], + ), + ) -@attr.s(frozen=True, slots=True) +@typechecked +@attr.s(auto_attribs=True, frozen=True, slots=True) class BoundTerm: - """Represents a binding of a named term to a fragment of source code. + """Represent a binding of a named term to a fragment of source code.""" - Attributes - ---------- term: str - The name of the term. location: LocationRange - The location range to which the term is bound. fragment: str - The source code to which the term is bound. - """ - term = attr.ib(type=str) - location = attr.ib(type=LocationRange) - fragment = attr.ib(type=str) @staticmethod - def from_dict(d: Dict[str, Any]) -> 'BoundTerm': - """Constructs a bound term from a dictionary-based description.""" - return BoundTerm(term=d['variable'], - location=LocationRange.from_dict(d['range']), - fragment=d['value']) + def from_dict(bound_term_dict: dict[str, str | dict[str, dict[str, int]]]) -> BoundTerm: + """ + Create a BoundTerm object from a dictionary representation. + + :param bound_term_dict: + :type bound_term_dict: dict[str, str | dict[str, dict[str, int]]] + """ + term = bound_term_dict["variable"] + location = bound_term_dict["range"] + fragment = bound_term_dict["value"] + + if not isinstance(term, str): + raise TypeError + + if not isinstance(location, dict): + raise TypeError + + if not isinstance(fragment, str): + raise TypeError + + return BoundTerm( + term=term, + location=LocationRange.from_dict(location), + fragment=fragment, + ) +@typechecked class Environment(Mapping[str, BoundTerm]): - @staticmethod - def from_dict(ts: Sequence[Dict[str, Any]]) -> 'Environment': - return Environment([BoundTerm.from_dict(bt) for bt in ts]) + """ + The `Environment` class represents a mapping of string keys to `BoundTerm` values. + + It provides methods to access and manipulate the mappings. + + """ + + def __init__(self: Environment, bindings: Sequence[BoundTerm]) -> None: + """ + Initialize an instance of the Environment class. - def __init__(self, bindings: Sequence[BoundTerm]) -> None: + :param bindings: + :type bindings: Sequence[BoundTerm] + """ + super().__init__() self.__bindings = {b.term: b for b in bindings} - def __repr__(self) -> str: - s = "comby.Environment([{}])" - return s.format(', '.join([repr(self[t]) for t in self])) + @staticmethod + def from_dict(ts: Sequence[dict[str, str | dict[str, dict[str, int]]]]) -> Environment: + """ + Create an Environment object from a list of dictionaries. + + :param ts: A list of dictionaries representing the bindings. + :type ts: Sequence[dict[str, str | dict[str, dict[str, int]]]] + :returns: An Environment object. + :rtype: Environment + """ + return Environment( + bindings=[BoundTerm.from_dict(bt) for bt in ts], + ) + + @override + def __repr__(self: Environment) -> str: + """ + Return a string representation of the Environment object. + + The string representation is a list of the bindings in the environment + + + :returns: A string representation of the Environment object. + + :rtype: str + """ + environment_str = "".join( + [ + "comby.Environment([])", + ], + ) + return environment_str.format( + ", ".join([repr(self[t]) for t in self]), + ) + + @override + def __len__(self: Environment) -> int: + """ + Return the number of bindings in the environment. + + Length of the environment. + - def __len__(self) -> int: - """Returns the number of bindings in this environment.""" + :returns: The number of bindings in the environment. + + :rtype: int + """ return len(self.__bindings) - def __iter__(self) -> Iterator[str]: - """Returns an iterator over the term names in this environment.""" - return self.__bindings.keys().__iter__() + @override + def __iter__(self: Environment) -> Iterator[str]: + """ + Return an iterator over the term names in the environment. + + The iterator yields the names of the bindings in the environment. + - def __getitem__(self, term: str) -> BoundTerm: - """Fetches details of a particular term within this environment. + :returns: An iterator over the term names in the environment. - Parameters: - term: the name of the term. + :rtype: Iterator[str] + """ + return self.__bindings.keys().__iter__() - Returns: - details of the source to which the term was bound. + @override + def __getitem__(self: Environment, term: str) -> BoundTerm: + """ + Return the `BoundTerm` value associated with the given term name. - Raises: - KeyError: if no term is found with the given name. + :param term: + :type term: str """ return self.__bindings[term] -@attr.s(slots=True, frozen=True) +@typechecked +@attr.s(auto_attribs=True, slots=True, frozen=True) class Match(Mapping[str, BoundTerm]): """ - Describes a single match of a given template in a source text as a mapping - of template terms to snippets of source code. + The `Match` class represents a mapping of string keys to `BoundTerm` values. + + It provides methods to access and manipulate the mappings. + + """ - Attributes - ---------- matched: str - the source text that was matched. location: LocationRange - the range of location range that was matched. environment: Environment - the associated environment, mapping template terms to snippets in the - source text, for the match. - """ - matched = attr.ib(type=str) - location = attr.ib(type=LocationRange) - environment = attr.ib(type=Environment) @staticmethod - def from_dict(d: Dict[str, Any]) -> 'Match': - return Match(matched=d['matched'], - location=LocationRange.from_dict(d['range']), - environment=Environment.from_dict(d['environment'])) + def from_dict(match_dict: dict[str, str | LocationRange | Environment]) -> Match: + """ + Create a `Match` object from a dictionary representation. + + :param match_dict: The dictionary representation of the `Match` object. + :type match_dict: dict[str, str | LocationRange | Environment] + :returns: The created `Match` object. + :rtype: Match + :raises TypeError: If the types of the values in the dictionary are not valid. + + """ + matched = match_dict["matched"] + location = match_dict["location"] + environment = match_dict["environment"] + + if not isinstance(matched, str): + raise TypeError + + if not isinstance(location, LocationRange): + raise TypeError + + if not isinstance(environment, Environment): + raise TypeError + + return Match( + matched=matched, + location=location, + environment=environment, + ) + + @override + def __len__(self: Match) -> int: + """ + Return the number of bindings in the environment. - def __len__(self) -> int: - """Returns the number of bindings in the environment.""" + The number of bindings in the environment. + + + :returns: The number of bindings in the environment. + + :rtype: int + """ return len(self.environment) - def __iter__(self) -> Iterator[str]: - """Returns an iterator over the term names in the environment.""" + @override + def __iter__(self: Match) -> Iterator[str]: + """ + Return an iterator over the term names in the environment. + + The iterator yields the names of the bindings in the environment. + + + :returns: An iterator over the term names in the environment. + + :rtype: Iterator[str] + """ yield from self.environment - def __getitem__(self, term: str) -> BoundTerm: + @override + def __getitem__(self: Match, term: str) -> BoundTerm: + """ + Return the `BoundTerm` value associated with the given term name from the environment. + + :param term: The term name. + :type term: str + :returns: The `BoundTerm` value associated with the term name. + :rtype: BoundTerm + """ return self.environment[term] + + +@typechecked +class ConfigMatch(TypedDict): + """Config match.""" + + matched: str + range: dict[str, dict[str, int]] + environment: Sequence[dict[str, str | dict[str, dict[str, int]]]] + + +@typechecked +class ConfigJson(TypedDict): + """Config json.""" + + matches: list[ConfigMatch] + uri: None + + +@typechecked +class ConfigLocation(TypedDict): + """Config location.""" + + start: dict[str, int] + end: dict[str, int] diff --git a/src/comby/exceptions.py b/src/comby/exceptions.py index 3157962..3a7d00a 100644 --- a/src/comby/exceptions.py +++ b/src/comby/exceptions.py @@ -1,31 +1,29 @@ -# -*- coding: utf-8 -*- -""" -This module provides definitions for the exceptions raised by Comby. -""" +"""This module provides definitions for the exceptions raised by Comby.""" import attr +__all__ = ["CombyExceptionError", "ConnectionFailureError"] -class CombyException(Exception): + +class CombyExceptionError(Exception): """Base class used by all Comby exceptions.""" -class ConnectionFailure(CombyException): +class ConnectionFailureError(CombyExceptionError): """ - The client failed to establish a connection to the server within the - allotted connection timeout window. + The client failed to establish a connection to the server. + + The client failed to establish a connection to the server within the allotted + connection timeout window. + + + :raises ConnectionFailureError: always + """ -@attr.s(auto_exc=True) -class CombyBinaryError(CombyException): - """An error was produced by the Comby binary. +@attr.s(auto_attribs=True) +class CombyBinaryError(CombyExceptionError): + """An error was produced by the Comby binary.""" - Attributes - ---------- code: int - The exit code that was produced by the binary. message: str - The error message that was produced by the binary. - """ - code = attr.ib(type=int) - message = attr.ib(type=str) diff --git a/src/comby/interface.py b/src/comby/interface.py index 6ba9830..d4a4f63 100644 --- a/src/comby/interface.py +++ b/src/comby/interface.py @@ -1,84 +1,126 @@ -# -*- coding: utf-8 -*- """ This module defines a standard interface for interacting with Comby. """ -__all__ = ('CombyInterface',) +from __future__ import annotations -from typing import Iterator, Dict, Optional import abc +from typing import TYPE_CHECKING, Iterator -from .core import Match +from typeguard import typechecked +__all__ = ("CombyInterface",) + +if TYPE_CHECKING: + from comby.core import Match + + +@typechecked class CombyInterface(abc.ABC): - """Provides a standard interface for interacting with Comby. - - Attributes - ---------- - version: str - The version of Comby that is provided by this interface. - language: str - The default language that should be assumed when dealing with source - text where no specific language is specified. - """ - @property - @abc.abstractmethod - def version(self) -> str: - ... + """Provides a standard interface for interacting with Comby.""" @property @abc.abstractmethod - def language(self) -> str: + def version(self: CombyInterface) -> str: + """ + Retrieve the version number of the Comby binary. + + This method accesses the version information of the Comby binary and returns it as a string. + If the object is not a CombyBinary instance, it raises a TypeError. + + + :returns: The version number of the Comby binary. + + :rtype: str + :raises TypeError: If the object is not an instance of CombyBinary. + + """ ... @abc.abstractmethod - def matches(self, - source: str, - template: str, - *, - language: Optional[str] = None - ) -> Iterator[Match]: - """Finds all matches of a given template within a source text. - - Parameters - ---------- - source: str - The source text to be searched. - template: str - The template that should be used for matching. - language: str, optional - Specifies the language of the source text. If no language is - specified, then the default language associated with this + def matches( + self: CombyInterface, + source: str, + template: str, + *, + language: str | None = None, + ) -> Iterator[Match]: + """ + Finds all matches of a given template within a source text. + + :param source: The source text to be searched. + :type source: str + :param template: The template that should be used for matching. + :type template: str + :param *: + :param language: Specifies the language of the source text. + If no language is specified, then the default language associated with this interface will be used. + :type language: str | None + :returns: An iterator over all matches of the given template within + :rtype: Iterator[Match] + :raises None: - Yields - ------ - All matches of the given template within in the text. """ ... @abc.abstractmethod - def substitute(self, - template: str, - args: Dict[str, str], - *, - language: Optional[str] = None - ) -> str: - """Substitutes a set of terms into a given template.""" + def substitute( + self: CombyInterface, + template: str, + args: dict[str, str], + *, + language: str | None = None, + ) -> str: + """ + Substitutes a set of terms into a given template. + + :param template: + :type template: str + :param args: + :type args: dict[str, str] + :param *: + :param language: (Default value = None) + :type language: str | None + :raises None: + + """ ... @abc.abstractmethod - def rewrite(self, - source: str, - match: str, - rewrite: str, - args: Optional[Dict[str, str]] = None, - *, - diff: bool = False, - language: Optional[str] = None - ) -> str: + def rewrite( + self: CombyInterface, + source: str, + match: str, + rewrite: str, + args: dict[str, str] | None = None, + *, + diff: bool = False, + language: str | None = None, + match_newline_at_toplevel: bool = False, + ) -> str: """ - Rewrites all matches of a template in a source text using a rewrite - template and an optional set of arguments to that rewrite template. + Rewrite all matches in source with rewrite template. + + Rewrites all matches of a template in a source text using a rewrite template and + an optional set of arguments to that rewrite template. + + :param source: + :type source: str + :param match: + :type match: str + :param rewrite: + :type rewrite: str + :param args: (Default value = None) + :type args: dict[str, str] | None + :param *: + :param diff: (Default value = False) + :type diff: bool + :param language: (Default value = None) + :type language: str | None + :param match_newline_at_toplevel: (Default value = False) + :type match_newline_at_toplevel: bool + :raises None: + """ ... diff --git a/src/comby/py.typed b/src/comby/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/test/test_binary.py b/test/test_binary.py deleted file mode 100644 index 42165b0..0000000 --- a/test/test_binary.py +++ /dev/null @@ -1,81 +0,0 @@ -# -*- coding: utf-8 -*- -import logging - -import pytest - -from comby import CombyBinary - - -@pytest.fixture -def comby(): - return CombyBinary() - - -def test_backslash_escapes_issue_32(comby): - source = " if \'charset\' in response.headers.get(\"Content-Type\", \"\")\n" - lhs = ":[ spaces]:[a]:[newline~[\n]]" - rhs = ":[spaces]:[a]:[newline]:[spaces]break;:[newline]" - matches = list(comby.matches(source, lhs, language=".py")) - assert len(matches) == 1 - match = matches[0] - - environment = {entry: match[entry].fragment for entry in match.environment} - mutated = comby.substitute(rhs, environment) - expected = " if 'charset' in response.headers.get(\"Content-Type\", \"\")\n break;\n" - assert mutated == expected - - -def test_match(comby): - source = "print('hello world')" - template = "print(:[1])" - matches = list(comby.matches(source, template)) - assert len(matches) == 1 - print(matches[0]) - - -def test_no_match(comby): - source = "foo" - template = "bar" - matches = list(comby.matches(source, template)) - print(matches) - assert len(matches) == 0 - - -def test_rewrite(comby): - source = "print('hello world')" - template = "print(:[1])" - rewrite = "println(:[1])" - expected = "println('hello world')" - actual = comby.rewrite(source, template, rewrite) - assert actual == expected - - source = """ - switch (name) { - case "WALL-E": - System.out.println("Hey! Stop that droid!"); - break; - default: - System.out.println("These aren't the droids we're looking for..."); - } - """.strip() - template = 'case "WALL-E"' - rewrite = 'case "C3PO"' - expected = """ - switch (name) { - case "C3PO": - System.out.println("Hey! Stop that droid!"); - break; - default: - System.out.println("These aren't the droids we're looking for..."); - } - """.strip() - actual = comby.rewrite(source, template, rewrite, language='.java') - assert actual == expected - - -def test_substitute(comby): - template = "my name is :[1]" - args = {'1': 'very secret'} - expected = 'my name is very secret' - actual = comby.substitute(template, args) - assert actual == expected diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..9a2574d --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Test Comby API.""" diff --git a/tests/test_binary.py b/tests/test_binary.py new file mode 100644 index 0000000..59946a3 --- /dev/null +++ b/tests/test_binary.py @@ -0,0 +1,227 @@ +"""Test the Comby binary.""" +import pytest + +from comby import CombyBinary + + +@pytest.fixture() +def get_comby_binary() -> CombyBinary: + """ + Create the Comby binary. + + """ + return CombyBinary() + + +def test_matches_should_return_single_match_when_pattern_contains_backslashes( + get_comby_binary: CombyBinary, +) -> None: + """ + Test that the 'matches' method accurately identifies patterns with backslashes in the source code. + + Verifies that the 'matches' method of a CombyBinary object can successfully find a pattern + that includes backslash escapes in a source string exactly once. + + :param get_comby_binary: A callable that returns a CombyBinary object. + :type get_comby_binary: CombyBinary + """ + source = ' if \'charset\' in response.headers.get("Content-Type", "")\n' + template = ":[ spaces]:[a]:[newline~[\n]]" + + result = list(get_comby_binary.matches(source, template, language=".py")) + + assert len(result) == 1 + + +def test_matches_should_return_correct_substitution_when_backslash_escapes_present( + get_comby_binary: CombyBinary, +) -> None: + """ + Test that the 'matches' method correctly substitutes patterns that include backslash escapes. + + Verifies that the 'matches' method of a CombyBinary object can correctly perform a substitution + when the source string contains backslash escapes. The substitution is considered correct if the + result matches the expected string exactly. + + :param get_comby_binary: A callable that returns a CombyBinary object. + :type get_comby_binary: CombyBinary + """ + source = ' if \'charset\' in response.headers.get("Content-Type", "")\n' + template = ":[ spaces]:[a]:[newline~[\n]]" + replacement = ":[spaces]:[a]:[newline]:[spaces]break;:[newline]" + matches = list(get_comby_binary.matches(source, template, language=".py")) + match = matches[0] + environment = {entry: match[entry].fragment for entry in match.environment} + expected = ' if \'charset\' in response.headers.get("Content-Type", "")\n break;\n' + + result = get_comby_binary.substitute(replacement, environment) + + assert result == expected + + +def test_matches_should_return_single_match_when_pattern_exists_once( + get_comby_binary: CombyBinary, +) -> None: + """ + Test that the 'matches' method successfully finds patterns in the source code. + + Verifies that the 'matches' method of a CombyBinary object can identify when a specified pattern + (template) occurs in a source string. A successful match is when the source contains the exact + pattern described by the template exactly once. + + :param get_comby_binary: A callable that returns a CombyBinary object. + :type get_comby_binary: CombyBinary + """ + source = "print('hello world')" + template = "print(:[1])" + + result = list(get_comby_binary.matches(source, template)) + + assert len(result) == 1 + + +def test_matches_should_return_no_matches_when_pattern_does_not_exist( + get_comby_binary: CombyBinary, +) -> None: + """ + Test that the 'matches' method returns no matches when the pattern does not exist in the source string. + + Verifies that the 'matches' method of a CombyBinary object accurately identifies the absence of a specified pattern + (template) within a source string. A successful test is when the source does not contain the specified pattern, + resulting in zero matches. + + :param get_comby_binary: A callable that returns a CombyBinary object. + :type get_comby_binary: CombyBinary + """ + source = "foo" + template = "bar" + + result = list(get_comby_binary.matches(source, template)) + + assert len(result) == 0 + + +def test_rewrite_should_succeed_when_template_matches_once( + get_comby_binary: CombyBinary, +) -> None: + """ + Test that the 'rewrite' method successfully applies transformations. + + Verifies that the 'rewrite' method of a CombyBinary object can apply a specified transformation + when the source string contains the pattern described by the template exactly once. + + :param get_comby_binary: A callable that returns a CombyBinary object. + :type get_comby_binary: CombyBinary + """ + source = "print('hello world')" + template = "print(:[1])" + rewrite = "println(:[1])" + expected = "println('hello world')" + + result = get_comby_binary.rewrite(source, template, rewrite) + + assert result == expected + + +def test_rewrite_switch_case_to_c3po( + get_comby_binary: CombyBinary, +) -> None: + """ + Test that the 'rewrite' method can successfully replace a specific case in a switch statement. + + Verifies that the 'rewrite' method of a CombyBinary object can identify and replace a specified pattern + (template) in a source string with a desired replacement pattern. The test checks for a correct rewrite + when replacing the case label "WALL-E" with "C3PO" within a switch statement. + + :param get_comby_binary: A callable that returns a CombyBinary object. + :type get_comby_binary: CombyBinary + """ + source: str = """ + switch (name) { + case "WALL-E": + System.out.println("Hey! Stop that droid!"); + break; + default: + System.out.println("These aren't the droids we're looking for..."); + } + """.strip() + template = 'case "WALL-E"' + rewrite = 'case "C3PO"' + expected = """ + switch (name) { + case "C3PO": + System.out.println("Hey! Stop that droid!"); + break; + default: + System.out.println("These aren't the droids we're looking for..."); + } + """.strip() + + result = get_comby_binary.rewrite(source, template, rewrite, language=".java") + + assert result == expected + + +def test_rewrite_should_match_newline_at_top_of_source( + get_comby_binary: CombyBinary, +) -> None: + """ + Test that the 'rewrite' method matches and rewrites code at the top level of the source code. + + Verifies that the 'rewrite' method of a CombyBinary object can identify and transform a specified pattern + (template) that occurs at the top level of a source string, where the pattern includes a newline character + at the beginning of the source string. + + :param get_comby_binary: A callable that returns a CombyBinary object. + :type get_comby_binary: CombyBinary + """ + source = "\n".join( + [ + "def foo():", + " print('hello world')", + "", + ], + ) + template = "def :[fn_name]::[body]:[next_fn~(\n\\z)]" + rewrite = "\n".join( + [ + "def :[fn_name]::[body]", + " print('hello mars'):[next_fn]", + ], + ) + expected = "\n".join( + [ + "def foo():", + " print('hello world')", + " print('hello mars')", + "", + ], + ) + + result = get_comby_binary.rewrite( + source, template, rewrite, language=".py", match_newline_at_toplevel=True + ) + + assert result == expected + + +def test_substitute_should_replace_pattern_with_arguments( + get_comby_binary: CombyBinary, +) -> None: + """ + Test that the 'substitute' method replaces patterns with provided arguments. + + Verifies that the 'substitute' method of a CombyBinary object can accurately replace a specified pattern + (template) with the arguments provided. A successful substitution is when the template is replaced by + the arguments exactly as specified. + + :param get_comby_binary: A callable that returns a CombyBinary object. + :type get_comby_binary: CombyBinary + """ + template = "my name is :[1]" + args = {"1": "very secret"} + expected = "my name is very secret" + + result = get_comby_binary.substitute(template, args) + + assert result == expected From d378ef3218983465c02d45624a985433c8e56fb0 Mon Sep 17 00:00:00 2001 From: tomas Date: Tue, 7 Nov 2023 15:16:53 -0800 Subject: [PATCH 4/4] Add noxfile.py to manage testing and linting sessions This change includes the addition of noxfile.py, setting up sessions for running tests, linters, type checking, and code formatting. --- noxfile.py | 240 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 noxfile.py diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 0000000..f0b6c71 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,240 @@ +"""My noxfile.py.""" +from __future__ import annotations + +import re +from pathlib import Path +from tempfile import NamedTemporaryFile + +import nox +from nox_poetry import Session, session + +__all__ = [ + "fmt", + "fmt_check", + "lint", + "lint_fix", + "type_check", + "test", +] + + +nox.options.error_on_external_run = True +nox.options.reuse_existing_virtualenvs = True +nox.options.sessions = [ + "fmt_check", + "lint", + "type_check", + "test", +] + +FILES: list[str] = [ + "src/comby/binary.py", + "src/comby/core.py", + "src/comby/exceptions.py", + "src/comby/interface.py", + "tests", + "noxfile.py", +] + + +@nox.session(venv_backend="none") +def test(project: Session) -> None: + """ + Run project's tests. + + :param project: + :type project: Session + """ + _ = project.run( + # "-m", + "pytest", + "--cov=src", + "--cov-report=html", + "--cov-report=term", + "tests", + # *project.posargs, + ) + + +@nox.session(venv_backend="none") +@nox.parametrize("file_path", FILES) +def fmt(project: Session, file_path: str) -> None: + """ + Run the project's formatting tools. + + :param project: + :type project: Session + :param file_path: + :type file_path: str + """ + _ = project.run( + "pyment", + "--first-line", + "False", + "--output", + "reST", + "--ignore-private", + "False", + "--init2class", + "--skip-empty", + "--write", + file_path, + ) + + _ = project.run( + "ruff", + "check", + file_path, + "--select", + "I", + "--fix", + ) + _ = project.run( + "black", + file_path, + ) + + +@nox.session(venv_backend="none") +@nox.parametrize("file_path", FILES) +def fmt_check(project: Session, file_path: str) -> None: + """ + Run the project's formatter checking tools. + + :param project: + :type project: Session + :param file_path: + :type file_path: str + """ + _ = project.run( + "ruff", + "check", + file_path, + "--select", + "I", + ) + _ = project.run( + "black", + "--check", + file_path, + ) + + +@session(venv_backend="none") +@nox.parametrize("file_path", FILES) +def lint(project: Session, file_path: str) -> None: + """ + Run the project's linter tools. + + :param project: + :type project: Session + :param file_path: + :type file_path: str + """ + _ = project.run( + "ruff", + "check", + file_path, + ) + _ = project.run("flake8", file_path) + + +@session(venv_backend="none") +@nox.parametrize("file_path", FILES) +def lint_fix(project: Session, file_path: str) -> None: + """ + Run the project's lint fixing tools. + + :param project: + :type project: Session + :param file_path: + :type file_path: str + """ + _ = project.run( + "ruff", + "check", + file_path, + "--fix", + ) + + +@session(venv_backend="none") +def update_typing(project: Session) -> None: + """ + Run the project's type checking tools. + + :param project: + :type project: Session + """ + files = get_file_paths(include_pattern=r"\.py$", exclude_pattern=r"^\..*$") + for ext in files: + _ = project.run("python-typing-update", str(ext)) + + +@session(venv_backend="none") +@nox.parametrize("file_path", FILES) +def type_check(project: Session, file_path: str) -> None: + """ + Run the project's type checking tools. + + :param project: + :type project: Session + :param file_path: + :type file_path: str + """ + _ = project.run("mypy", file_path) + + +doc_env = {"PYTHONPATH": "src"} + + +@session(reuse_venv=False) +def licenses(project: Session) -> None: + """ + Run the project's license tools. + + :param project: + :type project: Session + """ + # Generate a unique temporary file name. Poetry cannot write to the temp file directly on + # Windows, so only use the name and allow Poetry to re-create it. + with NamedTemporaryFile() as t: + requirements_file = Path(t.name) + + # Install dependencies without installing the package itself: + # https://github.com/cjolowicz/nox-poetry/issues/680 + _ = project.run_always( + "poetry", + "export", + "--without-hashes", + f"--output={requirements_file}", + external=True, + ) + project.install("pip-licenses", "-r", str(requirements_file)) + _ = project.run("pip-licenses", *project.posargs) + requirements_file.unlink() + + +# Note: This reuse_venv does not yet have affect due to: +# https://github.com/wntrblm/nox/issues/488 +def get_file_paths(include_pattern: str = "", exclude_pattern: str = "") -> list[str]: + """ + Get a list of file paths that match the given include and exclude patterns. + + :param include_pattern: (Default value = "") + :type include_pattern: str + :param exclude_pattern: (Default value = "") + :type exclude_pattern: str + """ + base_path = Path() + file_paths = [] + include_regex = re.compile(include_pattern) + exclude_regex = re.compile(exclude_pattern) + for path in base_path.rglob("*"): + path_str = str(path) + if exclude_regex.search(path_str): + continue + if not include_regex.search(path_str): + continue + file_paths.append(path_str) + return file_paths