From f99109fcaa0fcbcafe51cff49beb95c8974b8e2f Mon Sep 17 00:00:00 2001 From: msftcangoblowme Date: Fri, 20 Sep 2024 08:57:42 +0000 Subject: [PATCH] add dependency drain-swamp-snippet - feat: use package drain-swamp-snippet - refactor: remove module drain_swamp.snip - chore: add build requirement dependency drain-swamp-snippet --- CHANGES.rst | 4 + docs/_toc.yml | 6 +- docs/code/index.rst | 8 +- docs/code/snip.rst | 9 - docs/conf.py | 4 + docs/getting_started/pipenv-unlock.rst | 9 +- docs/getting_started/setuptools-scm.rst | 9 +- docs/objects-dss.inv | Bin 0 -> 351 bytes docs/objects-dss.txt | 16 + docs/requirements.in | 2 +- docs/requirements.lock | 6 +- docs/requirements.unlock | 1 + docs/snippets/index.rst | 8 - docs/snippets/snippets.rst | 234 ----- docs/troubleshooting/build-fail.rst | 3 + howto.txt | 8 +- pyproject.toml | 1 + requirements/dev.in | 2 +- requirements/dev.lock | 8 +- requirements/dev.unlock | 1 + requirements/kit.lock | 2 +- requirements/manage.in | 4 +- requirements/manage.lock | 2 + requirements/manage.unlock | 1 + requirements/pip-tools.in | 2 +- requirements/pip-tools.lock | 6 +- requirements/pip.lock | 2 +- requirements/prod.in | 1 + requirements/prod.lock | 4 +- requirements/prod.unlock | 1 + requirements/tox.lock | 2 +- src/drain_swamp/_version.py | 4 +- src/drain_swamp/cli_igor.py | 8 +- src/drain_swamp/cli_unlock.py | 18 +- .../monkey/plugins/ds_refresh_links.py | 3 +- src/drain_swamp/snip.py | 872 ------------------ src/drain_swamp/snip.pyi | 59 -- src/drain_swamp/snippet_pyproject_toml.py | 11 +- src/drain_swamp/snippet_pyproject_toml.pyi | 2 +- src/drain_swamp/snippet_sphinx_conf.py | 11 +- tests/_bad_files/backend_only.pyproject_toml | 10 +- .../complete-version-other.pyproject_toml | 10 +- .../_bad_files/snippet-nested.pyproject_toml | 10 +- .../_bad_files/static-version.pyproject_toml | 10 +- .../static_dependencies.pyproject_toml | 10 +- tests/_good_files/backend-only.pyproject_toml | 10 +- .../backend-unsupported.pyproject_toml | 10 +- .../complete-lnk-files.pyproject_toml | 10 +- ...lete-manage-pip-prod-unlock.pyproject_toml | 10 +- .../complete-version-txt.pyproject_toml | 10 +- tests/_good_files/complete.pyproject_toml | 10 +- .../multiple-snippets.pyproject_toml | 10 +- tests/_good_files/no_copyright.pyproject_toml | 10 +- .../no_project_name.pyproject_toml | 10 +- .../_good_files/nonsense-keys.pyproject_toml | 10 +- .../_good_files/requires-none.pyproject_toml | 10 +- .../thin-wrap-backend.pyproject_toml | 10 +- .../weird_copyright-2.pyproject_toml | 10 +- .../weird_copyright.pyproject_toml | 10 +- tests/_project/install_minimum.pyproject_toml | 10 +- tests/test_backend_abc.py | 2 +- tests/test_snip.py | 645 ------------- 62 files changed, 303 insertions(+), 1898 deletions(-) delete mode 100644 docs/code/snip.rst create mode 100644 docs/objects-dss.inv create mode 100644 docs/objects-dss.txt delete mode 100644 docs/snippets/index.rst delete mode 100644 docs/snippets/snippets.rst delete mode 100644 src/drain_swamp/snip.py delete mode 100644 src/drain_swamp/snip.pyi delete mode 100644 tests/test_snip.py diff --git a/CHANGES.rst b/CHANGES.rst index eb755ec..a0263f3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -42,6 +42,10 @@ Changelog Commit items for NEXT VERSION .............................. + - feat: use package drain-swamp-snippet + - refactor: remove module drain_swamp.snip + - chore: add build requirement dependency drain-swamp-snippet + .. scriv-start-here .. _changes_1-7-2: diff --git a/docs/_toc.yml b/docs/_toc.yml index f4ae153..151a312 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -18,10 +18,11 @@ subtrees: - entries: - file: code/index entries: - - file: code/snip - file: code/cli_unlock - file: code/cli_igor - file: code/cli_scm_version + - title: Snip + url: https://drain-swamp-snippet.readthedocs.io/en/stable/code/snip.html#drain_swamp_snippet.snip.Snip - file: code/general/index entries: - file: code/general/version_file @@ -71,7 +72,8 @@ subtrees: - file: code/monkey/plugins/scm_version - caption: Articles entries: - - file: snippets/snippets + - title: Anatomy of a snippet + url: https://drain-swamp-snippet.readthedocs.io/en/stable/overview.html - file: article-version-specifiers - file: why/about_authors - file: why/cringe-culture diff --git a/docs/code/index.rst b/docs/code/index.rst index 63d7ae8..36badc6 100644 --- a/docs/code/index.rst +++ b/docs/code/index.rst @@ -17,10 +17,14 @@ Code manual - :doc:`cli_igor` - :doc:`cli_scm_version` - .. grid-item-card:: :material-twotone:`pinch;2em;sd-text-primary` Techniques + .. grid-item-card:: :material-twotone:`pinch;2em;sd-text-primary` Snippet :class-card: sd-border-0 + :link-type: url + :link: https://drain-swamp-snippet.readthedocs.io/en/stable/code/snip.html#drain_swamp_snippet.snip.Snip + :link-alt: Snippet base package drain-swamp-snippet has class Snip and enum ReplaceResult - - :doc:`Snip ` + - drain_swamp_snippet.Snip + - drain_swamp_snippet.ReplaceResult .. grid-item-card:: :material-twotone:`lock_open;2em;sd-text-success` Dependency locking :class-card: sd-border-0 diff --git a/docs/code/snip.rst b/docs/code/snip.rst deleted file mode 100644 index fe81de6..0000000 --- a/docs/code/snip.rst +++ /dev/null @@ -1,9 +0,0 @@ -Snip core -========== - -.. automodule:: drain_swamp.snip - :members: - :undoc-members: - :exclude-members: PATTERN_W_ID, TOKEN_START, TOKEN_END, VALIDATE_FAIL, REPLACED, NO_CHANGE, NO_MATCH - :platform: Unix - :synopsis: Within a snippet, replace a text block diff --git a/docs/conf.py b/docs/conf.py index 7be7d48..7ecb7ca 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -159,6 +159,10 @@ "https://github.com/pypa/setuptools/blob", ("objects-setuptools.inv", "objects-setuptools.txt"), ), + "dss": ( + "https://drain-swamp-snippet.readthedocs.io/en/stable", + ("objects-dss.inv", "objects-dss.txt"), + ), } intersphinx_disabled_reftypes = ["std:doc"] diff --git a/docs/getting_started/pipenv-unlock.rst b/docs/getting_started/pipenv-unlock.rst index 6a613d7..79bfaf8 100644 --- a/docs/getting_started/pipenv-unlock.rst +++ b/docs/getting_started/pipenv-unlock.rst @@ -49,7 +49,14 @@ Then link this to your ``pyproject.toml`` file .. code:: text [build-system] - requires = ["setuptools>=70.0.0", "wheel", "build", "setuptools_scm>=8", "drain_swamp"] + requires = [ + "setuptools>=70.0.0", + "wheel", + "build", + "setuptools_scm>=8", + "drain-swamp", + "drain-swamp-snippet", + ] build-backend = "setuptools.build_meta" [project] diff --git a/docs/getting_started/setuptools-scm.rst b/docs/getting_started/setuptools-scm.rst index b84593b..5500fe1 100644 --- a/docs/getting_started/setuptools-scm.rst +++ b/docs/getting_started/setuptools-scm.rst @@ -7,7 +7,14 @@ pyproject.toml .. code-block:: text [build-system] - requires = ["setuptools>=70.0.0", "wheel", "build", "setuptools_scm>=8", "drain-swamp"] + requires = [ + "setuptools>=70.0.0", + "wheel", + "build", + "setuptools_scm>=8", + "drain-swamp", + "drain-swamp-snippet", + ] build-backend = "setuptools.build_meta" [project] diff --git a/docs/objects-dss.inv b/docs/objects-dss.inv new file mode 100644 index 0000000000000000000000000000000000000000..663a702c20572bbb1cbcc3b449d748d91dcb0b2a GIT binary patch literal 351 zcmY#Z2rkIT%&Sny%qvUHE6FdaR47X=D$dN$Q!wIERtPA{&q_@$u~JAWO3cjDEiO;Y zEzm8_%Pc5JEm1JgGte{CE66V{F$5|NgJ`ot6AsBpRVYf$1?o;oEmFu&Qz*&EELKR% z%t=)M(#iR`1x2aF#i=O@rNx(w7`;U}Z3O|rJ$#Gdm zKhaZL|7hoVm$tVPyoL8QED`HvXk*m97(VNLihn~XyJ>>st{+oZ)Li&>`CiYrio&y} zAGaLcp?kr$<^S@P`3LqqiG8{0!JcV%Fa1nA@h+%*a(MJ_&pT1x4SSmUEY26Nlk_we mNH@_lh0kBh-2wnwexH{B literal 0 HcmV?d00001 diff --git a/docs/objects-dss.txt b/docs/objects-dss.txt new file mode 100644 index 0000000..13b159e --- /dev/null +++ b/docs/objects-dss.txt @@ -0,0 +1,16 @@ +# Sphinx inventory version 2 +# Project: drain-swamp-snippet 0.0.1.post1 +# Version: 0.0.1.post1 +# The remainder of this file is compressed using zlib. +drain_swamp_snippet.Snip py:class 1 code/snip.html#drain_swamp_snippet.snip.Snip - +drain_swamp_snippet.Snip.replace py:method 1 code/snip.html#drain_swamp_snippet.snip.Snip.replace - +drain_swamp_snippet.Snip.replace.params.replacement py:parmeter 1 code/snip.html#drain_swamp_snippet.snip.Snip.replace - +drain_swamp_snippet.Snip.replace.params.id_ py:parmeter 1 code/snip.html#drain_swamp_snippet.snip.Snip.replace - +drain_swamp_snippet.ReplaceResult py:class 1 code/snip.html#drain_swamp_snippet.snip.ReplaceResult - +drain_swamp_snippet.snip.Snip py:class 1 code/snip.html#$ - +drain_swamp_snippet.snip.Snip.replace py:method 1 code/snip.html#$ - +drain_swamp_snippet.snip.Snip.replace.params.replacement py:parmeter 1 code/snip.html#$ - +drain_swamp_snippet.snip.Snip.replace.params.id_ py:parmeter 1 code/snip.html#$ - +drain_swamp_snippet.snip.ReplaceResult py:class 1 code/snip.html#$ - +snippets std:doc -1 snippets.html snippets +overview std:doc -1 overview.html Anatomy of a snippet diff --git a/docs/requirements.in b/docs/requirements.in index f111a70..6732f87 100644 --- a/docs/requirements.in +++ b/docs/requirements.in @@ -2,7 +2,7 @@ # For details: https://github.com/msftcangoblowm/drain-swamp/blob/master/NOTICE.txt # -c ../requirements/pins.lock --c ../requirements/prod.in +-r ../requirements/prod.in sphinx sphinx-pyproject diff --git a/docs/requirements.lock b/docs/requirements.lock index dba1046..b030216 100644 --- a/docs/requirements.lock +++ b/docs/requirements.lock @@ -48,6 +48,8 @@ domdf-python-tools==3.8.0.post2 # via # dom-toml # sphinx-pyproject +drain-swamp-snippet==1.0.0.post2 + # via -r ../requirements/prod.in idna==3.6 # via # requests @@ -94,7 +96,7 @@ myst-parser==2.0.0 # via sphinx-external-toc-strict natsort==8.4.0 # via domdf-python-tools -packaging==24.0 +packaging==24.1 # via # sphinx # sphinx-external-toc-strict @@ -154,7 +156,7 @@ sphinx-copybutton==0.5.2 # via -r docs/requirements.in sphinx-design==0.6.1 # via -r docs/requirements.in -sphinx-external-toc-strict==1.2.0 +sphinx-external-toc-strict==1.2.3.post0 # via -r docs/requirements.in sphinx-favicon==1.0.1 # via -r docs/requirements.in diff --git a/docs/requirements.unlock b/docs/requirements.unlock index 0d3bb20..49234e6 100644 --- a/docs/requirements.unlock +++ b/docs/requirements.unlock @@ -14,3 +14,4 @@ sphinx_design sphinx-tabs sphinx-favicon interrogate +drain-swamp-snippet==1.0.0.post1 diff --git a/docs/snippets/index.rst b/docs/snippets/index.rst deleted file mode 100644 index 0db2a99..0000000 --- a/docs/snippets/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -snippets -========= - -.. figure:: /_static/asset-scissor-cut-up.* - - Replace portion of file ... snip - -.. tableofcontents:: diff --git a/docs/snippets/snippets.rst b/docs/snippets/snippets.rst deleted file mode 100644 index 3387c48..0000000 --- a/docs/snippets/snippets.rst +++ /dev/null @@ -1,234 +0,0 @@ -Anatomy of a snippet -==================== - -There is a block of text within a configuration file which would like to replace. - -The only requirement is the file format should recognize pound symbol ``#`` as a comment. - -snippet code -------------- - -A snippet is a dynamic block in an otherwise static file. - -The snippet code identifies each snippet. If there is only one snippet, -it's optional. To future proof it, best practice is to set a snippet -code. There is no down side besides ... - -i'm going to make you watch really horrible movies! - -The snippet code should be a cringe cultural reference. Will also see -this in the changelog. - -In a changelog, an entry line seldomly uses ``- style:``. Especially not -as the first line. - -And it's sorta awkward seeing it in a changelog. - -Heh! Changelogs aren't supposed to be for entertainment - -.. card:: - :shadow: none - - A snippet **without** a snippet_co - ^^^ - - .. code:: text - - before snippet - # @@@ editable - code block - # @@@ end - after snippet - - +++ - - | If only one, can provide a snippet code or not. - | At your discretion. - | In the future, could this static file ever use more snippets? - -.. card:: - :shadow: none - - A snippet **with** a snippet_co - ^^^ - - .. code:: text - - before snippet - # @@@ i_am_a_snippet_co - code block - # @@@ end - after snippet - - +++ - - If there are multiple snippets, identifies the unique snippet - -.. card:: - :shadow: none - - Multiple snippets in one file - ^^^ - - .. code:: text - - before snippet - # @@@ i_am_a_snippet_co - code block - # @@@ end - some more content - # @@@ i_am_another_snippet_co - code block - # @@@ end - after snippet - -| Don't nest snippets -| Don't mismatch begin/end tags -| markdown not supported - -replace example ----------------- - -Replace the text within the snippet - -.. doctest:: with_id_replace - - >>> import tempfile - >>> import textwrap - >>> from pathlib import Path - >>> - >>> from drain_swamp.snip import Snip, ReplaceResult - >>> - >>> # prepare - >>> text = ( - ... "before snippet\n" - ... "# @@@ editable i_am_a_snippet_co\n" - ... "code block\n" - ... "# @@@ end\n" - ... "after snippet\n" - ... ) - >>> contents_existing = textwrap.dedent(text) - >>> - >>> contents_new = """new\ncontents\nhere""" - >>> - >>> text_expected = ( - ... "before snippet\n" - ... "# @@@ editable i_am_a_snippet_co\n" - ... "new\n" - ... "contents\n" - ... "here\n" - ... "# @@@ end\n" - ... "after snippet\n" - ... ) - >>> expected = textwrap.dedent(text_expected) - >>> - >>> with tempfile.TemporaryDirectory() as f_path: - ... path_f = Path(f_path) - ... - ... # prepare - ... path_some_conf = path_f / "some.conf" - ... chars_written = path_some_conf.write_text(contents_existing) - ... - ... # act - ... snip = Snip(path_some_conf) - ... is_success = snip.replace(contents_new, id_="i_am_a_snippet_co") - ... - ... actual = path_some_conf.read_text() - ... assert is_success == ReplaceResult.REPLACED - ... assert actual == expected - ... - >>> - -In a temporary folder, created a file, ``some.conf`` with contents, -*contents_existing*. - -The snippet, with id *i_am_a_snippet_co*, replace the contents with *contents_new*. - -:code:`textwrap.dedent("""\\` would normally be used to: - -- remove indention -- ignores the preceding newline - -Snip constructor parameter, is_quiet, turns off logging - -Validation ------------ - -validation occurs at the beginning of -:py:meth:`Snip.replace `. Failing -validation, replace will not proceed; file contents will be unaffected. - -Validation checks: - -- nesting - -- mismatching or out of order start / end tags - -Where to use -------------- - -Python package authors rarely write and publish just one python package. - -We write lots of packages! - -In each package, there is boilerplate code, not covered by unittests, -that is almost an exact copy as found in other packages. - -After a few published packages, this boilerplate code becomes a liability -and an eye sore. - -Code within ``Makefile`` or ``igor.py`` needs to brought under control. -Like a cancer, waiting to be exploited, less is more. - -Ideally cut out in its entirely; preferably, as much as reasonable. - -File formats -- supported - -Lines starting with pound sign **#** are considered comments: - -- python -- bash -- pyproject.toml -- Linux config files - -File formats -- tricky: - -- yaml - - Indention would need to be supplied with the content. There is no - :code:`indent=8` option - -File formats -- ill-suited (for now): - -- html - - Comment begin/end tokens are :code:`` - -- RestructuredText - - Comment token (period)(period)(space) - -- markdown - - Platform-independent comment - - .. code:: text - - (empty line) - [comment]: # (This actually is the most platform independent comment) - - The blank line before the comment line and maybe one afterwards would be tricky - - .. seealso:: - - `markdown comments `_ - -- Makefile - - Makefile contains two languages: Makefile and bash (or whatever shell is set). - So there are two distinct languages in one file. Intertwined! - - Isn't autotools meant to build Makefiles? Isn't this also a sewer - targetted by hackers? - - The entire point is to reduce Makefile and igor.py code to the minimum. diff --git a/docs/troubleshooting/build-fail.rst b/docs/troubleshooting/build-fail.rst index e4b0715..7ee8ed1 100644 --- a/docs/troubleshooting/build-fail.rst +++ b/docs/troubleshooting/build-fail.rst @@ -32,6 +32,9 @@ To see the verbose error message * Creating isolated environment: venv+pip... * Installing packages in isolated environment: - build + - click + - drain-swamp-snippet + - pluggy - setuptools>=70.0.0 - setuptools_scm>=8 - wheel diff --git a/howto.txt b/howto.txt index 7afc7f5..96af206 100644 --- a/howto.txt +++ b/howto.txt @@ -46,10 +46,10 @@ - Check that the docs build correctly: $ tox -e docs or - $ cd docs && make doctest && cd - &>/dev/null - $ cd docs && make linkcheck && cd - &>/dev/null - $ cd docs && make html && cd - &>/dev/null - $ cd docs && make pdf && cd - &>/dev/null + $ cd docs && make doctest; cd - &>/dev/null + $ cd docs && make linkcheck; cd - &>/dev/null + $ cd docs && make html; cd - &>/dev/null + $ cd docs && make pdf; cd - &>/dev/null - tox will affect _version.py, revert version str back to tagged version - commit the release-prep changes $ make relcommit1 diff --git a/pyproject.toml b/pyproject.toml index ceeeb33..b5ff515 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ requires = [ "setuptools_scm>=8", "click", "pluggy", + "drain-swamp-snippet", ] build-backend = "_req_links.backend" backend-path = [ diff --git a/requirements/dev.in b/requirements/dev.in index 565bdae..e5759e7 100644 --- a/requirements/dev.in +++ b/requirements/dev.in @@ -2,7 +2,7 @@ # For details: https://github.com/msftcangoblowm/drain-swamp/blob/master/NOTICE.txt -c pins.in --c prod.in +-r prod.in black blackdoc diff --git a/requirements/dev.lock b/requirements/dev.lock index e0c5f0e..c6c703a 100644 --- a/requirements/dev.lock +++ b/requirements/dev.lock @@ -14,7 +14,7 @@ charset-normalizer==3.3.2 # via requests click==8.1.7 # via - # -c requirements/prod.in + # -r requirements/prod.in # black coverage[toml]==7.5.0 # via @@ -24,6 +24,8 @@ cryptography==42.0.5 # via secretstorage docutils==0.21.2 # via readme-renderer +drain-swamp-snippet==1.0.0.post2 + # via -r prod.in exceptiongroup==1.2.1 # via pytest fastjsonschema==2.19.1 @@ -78,7 +80,7 @@ mypy-extensions==1.0.0 # mypy nh3==0.2.17 # via readme-renderer -packaging==24.0 +packaging==24.1 # via # black # pytest @@ -92,7 +94,7 @@ platformdirs==4.2.1 # via black pluggy==1.5.0 # via - # -c requirements/prod.in + # -r requirements/prod.in # pytest pycodestyle==2.11.1 # via flake8 diff --git a/requirements/dev.unlock b/requirements/dev.unlock index 4563998..7247fa7 100644 --- a/requirements/dev.unlock +++ b/requirements/dev.unlock @@ -16,3 +16,4 @@ flake8-pyi twine click coverage +drain-swamp-snippet diff --git a/requirements/kit.lock b/requirements/kit.lock index d50179f..6e84859 100644 --- a/requirements/kit.lock +++ b/requirements/kit.lock @@ -16,7 +16,7 @@ filelock==3.13.4 # via cibuildwheel importlib-metadata==7.1.0 # via build -packaging==24.0 +packaging==24.1 # via # auditwheel # build diff --git a/requirements/manage.in b/requirements/manage.in index 70fd0d8..fdfff09 100644 --- a/requirements/manage.in +++ b/requirements/manage.in @@ -4,8 +4,8 @@ # packaging and kitting -c pins.in --c prod.in --c tox.in +-r prod.in +-r tox.in # pypi.org twine diff --git a/requirements/manage.lock b/requirements/manage.lock index cb014c9..e84cf7c 100644 --- a/requirements/manage.lock +++ b/requirements/manage.lock @@ -18,6 +18,8 @@ docutils==0.21.2 # via # readme-renderer # restview +drain-swamp-snippet==1.0.0.post2 + # via -r prod.in filelock==3.13.4 # via virtualenv identify==2.5.36 diff --git a/requirements/manage.unlock b/requirements/manage.unlock index 3983372..afd112f 100644 --- a/requirements/manage.unlock +++ b/requirements/manage.unlock @@ -8,3 +8,4 @@ tox colorama>=0.4.6 pip-tools click +drain-swamp-snippet diff --git a/requirements/pip-tools.in b/requirements/pip-tools.in index bb31486..c8e7963 100644 --- a/requirements/pip-tools.in +++ b/requirements/pip-tools.in @@ -1,7 +1,7 @@ # Licensed under the AGPLv3+ License: https://www.gnu.org/licenses/ # For details: https://github.com/msftcangoblowm/drain-swamp/blob/master/NOTICE.txt --c pip.in +-r pip.in # pip-tools commit 53309647980e2a4981db54c0033f98c61142de0b # Supposed to fix the absolute path issue, but didn't diff --git a/requirements/pip-tools.lock b/requirements/pip-tools.lock index aaf3b60..4316d46 100644 --- a/requirements/pip-tools.lock +++ b/requirements/pip-tools.lock @@ -4,7 +4,7 @@ click==8.1.7 # via pip-tools importlib-metadata==7.1.0 # via build -packaging==24.0 +packaging==24.1 # via build pip-tools==7.4.1 # via -r requirements/pip-tools.in @@ -25,9 +25,9 @@ zipp==3.20.0 # The following packages are considered to be unsafe in a requirements file: pip==24.0 # via - # -c requirements/pip.in + # -r requirements/pip.in # pip-tools setuptools==70.1.1 # via - # -c requirements/pip.in + # -r requirements/pip.in # pip-tools diff --git a/requirements/pip.lock b/requirements/pip.lock index 8258ee3..8c62dac 100644 --- a/requirements/pip.lock +++ b/requirements/pip.lock @@ -1,4 +1,4 @@ -packaging==24.0 +packaging==24.1 # via setuptools-scm setuptools-scm==8.0.4 # via -r requirements/pip.in diff --git a/requirements/prod.in b/requirements/prod.in index e3fb82a..da8d0c7 100644 --- a/requirements/prod.in +++ b/requirements/prod.in @@ -7,3 +7,4 @@ click # entrypoints pip-tools # lock requirements pluggy # plugin support in build backend and setuptools.finalize_distribution_options entrypoint setuptools-scm # get current version, `python setup.py --version` +drain-swamp-snippet diff --git a/requirements/prod.lock b/requirements/prod.lock index 9ab5b47..045c294 100644 --- a/requirements/prod.lock +++ b/requirements/prod.lock @@ -4,9 +4,11 @@ click==8.1.7 # via # -r requirements/prod.in # pip-tools +drain-swamp-snippet==1.0.0.post2 + # via -r prod.in importlib-metadata==7.1.0 # via build -packaging==24.0 +packaging==24.1 # via # build # setuptools-scm diff --git a/requirements/prod.unlock b/requirements/prod.unlock index 86c6e33..abd0d70 100644 --- a/requirements/prod.unlock +++ b/requirements/prod.unlock @@ -2,3 +2,4 @@ setuptools-scm pip-tools click pluggy +drain-swamp-snippet diff --git a/requirements/tox.lock b/requirements/tox.lock index 0306e4b..b3efa73 100644 --- a/requirements/tox.lock +++ b/requirements/tox.lock @@ -12,7 +12,7 @@ filelock==3.13.4 # via # tox # virtualenv -packaging==24.0 +packaging==24.1 # via # pyproject-api # tox diff --git a/src/drain_swamp/_version.py b/src/drain_swamp/_version.py index c7a6ec6..2208146 100644 --- a/src/drain_swamp/_version.py +++ b/src/drain_swamp/_version.py @@ -12,5 +12,5 @@ __version_tuple__: VERSION_TUPLE version_tuple: VERSION_TUPLE -__version__ = version = '1.7.2' -__version_tuple__ = version_tuple = (1, 7, 2) +__version__ = version = '1.8.0' +__version_tuple__ = version_tuple = (1, 8, 0) diff --git a/src/drain_swamp/cli_igor.py b/src/drain_swamp/cli_igor.py index a9c3e44..18fe1a6 100644 --- a/src/drain_swamp/cli_igor.py +++ b/src/drain_swamp/cli_igor.py @@ -80,6 +80,10 @@ from pathlib import Path import click +from drain_swamp_snippet import ( + ReplaceResult, + Snip, +) # pep366 ... # https://stackoverflow.com/a/34155199 @@ -144,10 +148,6 @@ seed_changelog, write_version_file, ) -from .snip import ( - ReplaceResult, - Snip, -) from .snippet_sphinx_conf import SnipSphinxConf _logger = logging.getLogger(f"{g_app_name}.cli_igor") diff --git a/src/drain_swamp/cli_unlock.py b/src/drain_swamp/cli_unlock.py index 1d2e7a7..e658c9b 100644 --- a/src/drain_swamp/cli_unlock.py +++ b/src/drain_swamp/cli_unlock.py @@ -24,6 +24,10 @@ ) import click +from drain_swamp_snippet import ( + ReplaceResult, + Snip, +) # pep366 ... # https://stackoverflow.com/a/34155199 @@ -99,10 +103,6 @@ refresh_links, unlock_compile, ) -from .snip import ( - ReplaceResult, - Snip, -) from .snippet_dependencies import SnippetDependencies from .snippet_pyproject_toml import ( SNIPPET_NO_MATCH, @@ -722,7 +722,15 @@ def create_links(path, is_set_lock, snippet_co): .. code-block:: text [build-system] - requires = ["setuptools>=70.0.0", "wheel", "build", "setuptools_scm>=8"] + requires = [ + "setuptools>=70.0.0", + "wheel", + "build", + "setuptools_scm>=8", + "click", + "pluggy", + "drain-swamp-snippet", + ] build-backend = "setuptools.build_meta" backend-path = ["_req_links"] diff --git a/src/drain_swamp/monkey/plugins/ds_refresh_links.py b/src/drain_swamp/monkey/plugins/ds_refresh_links.py index 7d1f4c2..07f257b 100644 --- a/src/drain_swamp/monkey/plugins/ds_refresh_links.py +++ b/src/drain_swamp/monkey/plugins/ds_refresh_links.py @@ -18,6 +18,8 @@ from pathlib import Path from typing import Any +from drain_swamp_snippet import ReplaceResult + from drain_swamp.backend_abc import BackendType from drain_swamp.check_type import click_bool from drain_swamp.exceptions import ( @@ -28,7 +30,6 @@ from drain_swamp.lock_toggle import refresh_links from drain_swamp.monkey.hooks import markers from drain_swamp.monkey.hooks.constants import HOOK_NAMESPACE -from drain_swamp.snip import ReplaceResult from drain_swamp.snippet_pyproject_toml import ( SNIPPET_NO_MATCH, SNIPPET_VALIDATE_FAIL, diff --git a/src/drain_swamp/snip.py b/src/drain_swamp/snip.py deleted file mode 100644 index d3d213f..0000000 --- a/src/drain_swamp/snip.py +++ /dev/null @@ -1,872 +0,0 @@ -""" -.. moduleauthor:: Dave Faulkmore - -Be able to search and replace editable regions within text files - -.. py:data:: __all__ - :type: tuple[str, str] - :value: ("Snip", "ReplaceResult") - - Modules exports - -.. py:data:: _logger - :type: logging.Logger - - Module level logger - -.. py:data:: mod_dotted_path - :type: str - :value: "drain_swamp.snip" - - Module dotted path - -.. py:data:: is_module_debug - :type: bool - - Module level debug flag - -""" - -from __future__ import annotations - -import logging -import re -import string -import sys -from enum import ( - Enum, - auto, -) -from functools import partial -from pathlib import ( - Path, - PurePath, -) - -from .check_type import is_ok -from .constants import g_app_name - -__package__ = "drain_swamp" -__all__ = ( - "Snip", - "ReplaceResult", -) - -mod_dotted_path = f"{g_app_name}.snip" -_logger = logging.getLogger() -is_module_debug = False - - -class ReplaceResult(Enum): - """Snippet replace result possibilities. - - For membership checking, equality comparison is supported - - .. code-block:: python - - oh_this_is_bad = ReplaceResult.VALIDATE_FAIL - assert oh_this_is_bad == ReplaceResult.VALIDATE_FAIL - - hmm_looks_promising = ReplaceResult.REPLACED - assert hmm_looks_promising != oh_this_is_bad - - .. py:attribute:: VALIDATE_FAIL - - Validation failed. Either nested or no matching start/end token - - .. py:attribute:: NO_MATCH - - No snippet with specified snippet code - - .. py:attribute:: REPLACED - - Snippet contents replaced - - .. py:attribute:: NO_CHANGE - - Snippet contents same as existing, so replace skipped - - """ - - VALIDATE_FAIL = auto() - NO_MATCH = auto() - REPLACED = auto() - NO_CHANGE = auto() - - def __eq__(self, other): - """Equality check. - - :param other: Should be same Enum class - :type other: typing.Any - :returns: True if equal otherwise False - :rtype: bool - """ - return self.__class__ is other.__class__ and other.value == self.value - - -def check_matching_tag_count( - contents, - token_start=None, - token_end=None, -): - """Checks tag count. Completely oblivious to overlapping tags. - - Non-overlapping start/end tokens - - .. code-block:: python - - from drain_swamp.snip import check_matching_tag_count, Snip - - token_start = Snip.TOKEN_START - token_end = Snip.TOKEN_END - contents = f"{token_start}{token_end}{token_start}{token_end}" - - is_ok = check_matching_tag_count( - contents, - token_start, - token_end, - ) - assert is_ok - - :param contents: file contents to be validated - :type contents: str - :param token_start: Default None. Regex indicating start of a snippet - :type token_start: str | None - :param token_end: Default None. Regex indicating end of a snippet - :type token_end: str | None - :returns: True is check successful otherwise False - :rtype: bool - """ - fcn_path = f"{mod_dotted_path}.check_matching_tag_count" - if is_ok(contents) and is_ok(token_start) and is_ok(token_end): - start_count = contents.count(token_start) - end_count = contents.count(token_end) - - if is_module_debug: # pragma: no cover - _logger.debug(f"{fcn_path} start token count: {start_count}") - _logger.debug(f"{fcn_path} end token count: {end_count}") - else: # pragma: no cover - pass - - is_tag_count_match = start_count == end_count - - if is_module_debug: # pragma: no cover - _logger.debug(f"{fcn_path} token counts match: {is_tag_count_match}") - else: # pragma: no cover - pass - - ret = is_tag_count_match - else: - ret = False - - return ret - - -def check_not_nested_or_out_of_order( - contents, - token_start, - token_end, -): - """Check tag pairs are not nested / out of order. - - Assumes :py:func:`drain_swamp.snip.check_matching_tag_count` - already checked. - - :param contents: file contents to check - :type contents: str - :param token_start: Default None. Start token - :type token_start: str | None - :param token_end: Default None. End token - :type token_end: str | None - :returns: - - True on success False indicates give feedback and - **definitely do not** attempt replacing a snippet - - :rtype: bool - :raises: - - - :py:exc:`ValueError` -- Either no contents, no start token, - or no end token provided - - """ - ret = True - fcn_path = f"{mod_dotted_path}.check_not_nested_or_out_of_order" - - if is_ok(contents) and is_ok(token_start) and is_ok(token_end): - idx_current = 0 - found_next = True - while found_next is True: - idx_start = contents.find(token_start, idx_current) - idx_end = contents.find(token_end, idx_current) - - # Not errors: - # - no tag pairs at all - # - no next tags - is_no_more_tags = idx_start == -1 and idx_end == -1 - if is_no_more_tags: - found_next = False - continue - else: # pragma: no cover - pass - - # is_end_at_beginning = idx_end != -1 and idx_end == idx_current - is_no_start_tag = idx_start == -1 and idx_end != -1 - is_no_end_tag = idx_start != -1 and idx_end == -1 - is_both_exist = idx_start != -1 and idx_end != -1 - is_end_before_start = is_both_exist and idx_end < idx_start - is_bad = is_no_start_tag or is_no_end_tag or is_end_before_start - - # is_good - """ - is_start_before_end = ( - is_both_exist - and idx_start < idx_end - ) - """ - pass - - if is_bad: - ret = False - found_next = False - else: - # next start idx - idx_next_start = idx_end + len(token_end) + 1 - # advance past end tag - idx_current = idx_next_start - else: - # either no contents, no start token, or no end token - ret = False - - if is_module_debug: # pragma: no cover - msg_info = ( - f"{fcn_path} Either no contents, no start token, or no " - "end token provided" - ) - _logger.info(msg_info) - else: # pragma: no cover - pass - - return ret - - -def sanitize_id(id_=""): - """tokenize snippet code. alphanumeric + underscore. - - :param id_: - - Snippet code. Default empty str. If empty str or None or just - whitespace, considered as an empty string - - :type id_: typing.Any | None - :returns: The snippet code sanitized - :rtype: str - """ - if id_ is None: - # None - ret = "" - else: - if not isinstance(id_, str): - # unsupported type - ret = "" - else: - # hyphen --> underscore - ret = id_.replace("-", "_") - - # \w - allowed_chars = string.ascii_letters + "_" - # filter out any other character - ret = "".join(c for c in ret if c in allowed_chars) - - return ret - - -class Snip: - """jinja2 templates is time consuming to get the spacing right. - A snippet is an easier alternative - - .. py:attribute:: TOKEN_START - :type: str - :value: "# @@@ editable" - - Beginning token of an editable section. Optionally can be followed - by one whitespace and a str id. ID can contain alphanumberic - characters and underscore. If there is no id, a file can contain at - most one editable section - - .. py:attribute:: TOKEN_END - :type: str - :value: "# @@@ end\\n" - - End token denotes end of editable section. A trailing newline is expected - - .. py:attribute:: PATTERN_W_ID - :type: re.Pattern - - Compiled regex which allows for an optional id. Captures two groups: - id and contents. - - Removed regex which doesn't not support optional id for editable sections - - ``.*`` means capture greedily. ``.*?`` means shortest path match - - .. seealso:: - - ``(?s)`` means single line mode. equivalent to compile flag ``re.DOTALL`` - - `regex -- modifiers `_ - - :ivar fname: the file in/near the project to update - :vartype fname: str | pathlib.Path - - :raises: - - - :py:exc:`TypeError` -- Unsupported type, fname can be either a str or Path - - .. todo:: - - Take editable file paths from ``pyproject.toml`` - - """ - - TOKEN_START = "# @@@ editable" - TOKEN_END = "# @@@ end\n" - # PATTERN_WO_ID = re.compile(r"(?s)# @@@ editable\n.*# @@@ end\n") - PATTERN_W_ID = re.compile( - r"# @@@ editable\s?(\w*)?\n(.*?)\n# @@@ end\n", - flags=re.DOTALL, - ) - __slots__ = ("_path_file", "_contents", "_is_infer") - - def __init__( - self, - fname, - ): - """Class constructor.""" - super().__init__() - - # Not throughly validated - self.path_file = fname - - self._contents = None - self._is_infer = False - - @property - def path_file(self): - """Path has not necessarily been checked. Run thru a validator func. - - :returns: A raw path either relative or absolute - :rtype: pathlib.Path - """ - return self._path_file - - @path_file.setter - def path_file(self, val): - """path_file setter - - Does not check: - - - whether relative or absolute path - - - whether is known to have editable region and therefore allowed - - :param val: Should be a str or Path. Either relative or absolute - :type val: typing.Any - :raises: - - - :py:exc:`TypeError` -- Unsupported type, can be either str or Path - - """ - msg_exc = "Unsupported type, fname can be either a str or Path" - if val is None: - raise TypeError(msg_exc) - else: - if isinstance(val, str): - self._path_file = Path(val) - elif issubclass(type(val), PurePath): - self._path_file = val - else: - raise TypeError(msg_exc) - - def is_file_ok(self): - """Try to grab permitted editable files from ``pyproject.toml``. - - :returns: True if file has absolute and is permitted - :rtype: bool - - .. todo:: r/w ?? - - What about confirm: readable and writable! - - """ - # Replace this with a check of ``pyproject.toml`` settings - is_editable_file = True - path_f = self.path_file - ret = ( - path_f.exists() - and path_f.is_file() - and path_f.is_absolute() - and is_editable_file - ) - return ret - - @property - def is_infer(self): - """id is None or empty str. snippet_co is taken from snippet. - - :returns: True if infer snippet_co otherwise False - :rtype: bool - """ - return self._is_infer - - def get_file(self): - """Read the file. - - :returns: file contents - :rtype: str - :raises: - - - :py:exc:`ValueError` -- cannot read file or is empty - - :py:exc:`FileNotFoundError` -- file is not ok for whatever reason - - """ - cls = type(self) - meth_path = f"{mod_dotted_path}.{cls.__name__}.get_file" - if not self.is_file_ok() and is_module_debug: # pragma: no cover - msg_warn = f"{meth_path} Cannot update nonexistent file, {self.path_file!r}" - _logger.warning(msg_warn) - else: # pragma: no cover - pass - - if self.is_file_ok(): - ret = self.path_file.read_text(encoding="utf-8") - - if ret is None or len(ret) == 0: - # Expecting file to not be empty - # Also expecting to contain editable regions - msg_exc = "Expecting file to be non-empty" - raise ValueError(msg_exc) - else: # pragma: no cover - pass - else: # pragma: no cover - # file not ok - msg_exc = ( - f"{meth_path} file not ok. replace editable region ... " - f"skip {self.path_file!r}" - ) - raise FileNotFoundError(msg_exc) - - return ret - - def replace(self, replacement, id_=""): - """Find snippet with *id_*. If no *id_*, provided the file - may contain at most one snippet. - - *id_* should be a cringe worthy cultural reference to an object - or minor character! One id should be changed for every minor - version release or PR - - In the changelog, in the commit, the top most lines should - use - - .. code-block:: text - - *- style: * tags - - Explain the snippet_co (id_) cringe worthy cultural reference - - - :param replacement: Just the content. Will be substituted within the file - :type replacement: str - :param id_: - - Default empty string. So as to support multiple snippets within a file. - If only intended ever to have one snippet, empty string is appropriate - - :type id_: str | None - :returns: VALIDATE_FAIL, NO_MATCH, REPLACED, NO_CHANGE - :rtype: drain_swamp.snip.ReplaceResult - - :raises: - - - :py:exc:`TypeError` -- Unsupported type, *replacement* contents must be a str - - """ - cls = type(self) - meth_path = f"{mod_dotted_path}.{cls.__name__}.replace" - - if replacement is None or not isinstance(replacement, str): - msg_exc = "Unsupported type, replacement contents must be a str" - raise TypeError(msg_exc) - - id_ = sanitize_id(id_) - if is_module_debug: # pragma: no cover - _logger.info(f"{meth_path} id_ (user input; filtered): {id_}") - msg_info = f"{meth_path} replacement (user input filtered): {replacement!r}" - _logger.info(msg_info) - else: # pragma: no cover - pass - - def replace_fcn(matchobj): - """inline func for snippets. Which are editable regions within utf-8 text - files. Under the hood, The search and replace is using - :py:func:`re.sub` and a nasty regex - - - ``matchobj.group(0)`` - Entire snippet including start/end tokens. Non-greedy - will match first end token - - - ``matchobj.group(1)`` - snippet id. Can be empty string if no multiple id support - - - ``matchobj.group(2)`` - snippet contents - - Requires these outer func/meth variables: - - - token_start - - token_end - - id_ - - replacement - - Assumes the text is within a text file, not a stream nor a str. - - Example before and after - - snippet with an id - - .. code-block:: text - - id_ = "asdf" - replacement = "abc abc abc" - old_text = "zzzzzzzzzzzzz\n# @@@ editable asdf\nblah blah blah\n# @@@ end\nzzzzzzzzzzzz\n" - - expected = ( - "zzzzzzzzzzzzz\n# @@@ editable asdf\nabc abc abc\n# @@@ end\nzzzzzzzzzzzz\n" - ) - - snippet without an id - - .. code-block:: text - - id_ = "" - replacement = "abc abc abc" - old_text = "zzzzzzzzzzzzz\n# @@@ editable\nblah blah blah\n# @@@ end\nzzzzzzzzzzzz\n" - - expected = ( - "zzzzzzzzzzzzz\n# @@@ editable\nabc abc abc\n# @@@ end\nzzzzzzzzzzzz\n" - ) - - Only one snippet without an id allowed with a file. snippets - with id and **one** snippet without id are allowed - - :param matchobj: Regex match object. Provided when a match occurs - :type matchobj: re.Match - :returns: - - If not an actual match, return the original content - otherwise modify the content. Whatever returned will - replace the existing text - - :rtype: str | None - - .. seealso:: - - `re.sub `_ - - `regex tester web app `_ - - """ - if is_module_debug: # pragma: no cover - _logger.debug(f"matched region: {matchobj.group(0)}") - _logger.debug(f"current id: {matchobj.group(1)}") - _logger.debug(f"current contents: {matchobj.group(2)}") - _logger.debug(f"target id: {id_}") - _logger.debug(f"replacement: {replacement!r}") - else: # pragma: no cover - pass - - current_id = matchobj.group(1) - is_target_have_id = len(id_) != 0 - is_current_have_id = len(current_id) != 0 - - if is_target_have_id and is_current_have_id and current_id == id_: - # both have id and match - ret = f"{token_start} {id_}\n{replacement}\n{token_end}" - - if is_module_debug: # pragma: no cover - msg_debug = "both have id; match" - _logger.debug(msg_debug) - else: # pragma: no cover - pass - elif is_target_have_id and is_current_have_id and current_id != id_: - # both have id; no match - if is_module_debug: # pragma: no cover - msg_debug = "both have id; no match --> return unmodified" - _logger.debug(msg_debug) - else: # pragma: no cover - pass - ret = matchobj.group(0) - elif not is_target_have_id and not is_current_have_id: - ret = f"{token_start}\n{replacement}\n{token_end}" - if is_module_debug: # pragma: no cover - msg_debug = "both no id; match" - _logger.debug(msg_debug) - else: # pragma: no cover - pass - else: - if is_module_debug: # pragma: no cover - msg_debug = "not a match --> return unmodified" - _logger.debug(msg_debug) - else: # pragma: no cover - pass - ret = matchobj.group(0) - - if is_module_debug: # pragma: no cover - msg_debug = f"ret: {ret}" - _logger.debug(msg_debug) - else: # pragma: no cover - pass - - return ret - - # first match snippet - t_snippet_existing = self.contents(id_=id_) - if isinstance(t_snippet_existing, ReplaceResult): - # invalid file or one or more validation checks failed - ret = t_snippet_existing - else: - # snippet_existing = t_snippet_existing[0] - snippet_co_actual = t_snippet_existing[1] - # If snippet_co not provided and only one snippet, use inferred snippet_co - if self.is_infer: - id_ = snippet_co_actual - else: # pragma: no cover - pass - - # file contents - text_existing = self._contents - - # Run re.sub on all editable regions - token_start = cls.TOKEN_START - token_end = cls.TOKEN_END - new_text = re.sub(cls.PATTERN_W_ID, replace_fcn, text_existing) - - if is_module_debug: # pragma: no cover - msg_info = f"{meth_path} text (after re.sub): {new_text}" - _logger.info(msg_info) - else: # pragma: no cover - pass - - is_changed = new_text != text_existing - - if is_changed: - if is_module_debug: # pragma: no cover - msg_info = f"{meth_path} Updating {self.path_file!r}" - _logger.info(msg_info) - else: # pragma: no cover - pass - self.path_file.write_text(new_text) - ret = ReplaceResult.REPLACED - else: # pragma: no cover - ret = ReplaceResult.NO_CHANGE - - return ret - - def validate(self): - """Validate target file contents are safe. - - All checks must pass. Over time add additional checks - - :returns: All checks passed - :rtype: bool - """ - - # This is an initial check, so should return, not raise - try: - contents = self.get_file() - except (ValueError, FileNotFoundError): - msg_exc = ( - f"file not ok. replace editable region ... skip {self.path_file!r}" - ) - _logger.warning(msg_exc) - self._contents = None - return False - - cls = type(self) - - token_start = cls.TOKEN_START - token_end = cls.TOKEN_END - validators = ( - partial( - check_matching_tag_count, - contents, - token_start=token_start, - token_end=token_end, - ), - partial( - check_not_nested_or_out_of_order, - contents, - token_start=token_start, - token_end=token_end, - ), - ) - ret = all([validator() for validator in validators]) - - # file contents --> buffer, so don't have to get_file twice - if ret is True: - self._contents = contents - else: - msg_exc = "Validation checks fail" - _logger.warning(msg_exc) - self._contents = None - - return ret - - @property - def snippets(self): - """Get all snippets. No filtering by snippet_co. - - :returns: tuple of snippet_co and snippet contents - :rtype: list[tuple[str, str]] | ReplaceResult - """ - cls = type(self) - meth_path = f"{mod_dotted_path}.{cls.__name__}.snippets" - - # If True --> self._contents is a str | None. None if checks fail - is_valid = self.validate() - is_invalid_file = not is_valid - is_checks_fail = is_valid and self._contents is None - if is_invalid_file or is_checks_fail: - if is_module_debug: # pragma: no cover - msg_info = ( - f"{meth_path} Validation issue. Check file: {self.path_file!r}" - ) - _logger.info(msg_info) - else: # pragma: no cover - pass - - return ReplaceResult.VALIDATE_FAIL - - # str, not None - contents = self._contents - - pattern = cls.PATTERN_W_ID - prog = re.compile(pattern) - - # snippet_co can be used multiple times, so not a set - seq_ret = [] - for m in prog.finditer(contents): - if m is not None: - # id, snippet contents - seq_ret.append((m.group(1), m.group(2))) - else: # pragma: no cover - # no matches - pass - - if len(seq_ret) == 0: - return ReplaceResult.NO_MATCH - - return seq_ret - - def print(self): - """Human readable summary of snippets. - - :returns: tuple of snippet_co and snippet contents - :rtype: list[tuple[str, str]] | ReplaceResult - """ - snippets = self.snippets - if ( - isinstance(snippets, ReplaceResult) - and snippets == ReplaceResult.VALIDATE_FAIL - ): - msg = ( - "Snippet validation fail. Either nested or non-matching " - "start/end tokens" - ) - print(msg, file=sys.stderr) - elif isinstance(snippets, ReplaceResult) and snippets == ReplaceResult.NO_MATCH: - msg = "There are no snippets" - print(msg, file=sys.stderr) - else: - lst = [] - lst_ids = [] - lst_contents = [] - for snippet_co, snippet_content in snippets: - if len(snippet_co) == 0: - co = "(empty string)" - else: - co = snippet_co - lst_ids.append(co) - block = f"{co} \n\n{snippet_content}" - lst_contents.append(block) - str_header_0 = "snippet codes:" - str_header_1 = "blocks:" - lst.append(f"{str_header_0}\n") - lst.extend(lst_ids) - lst.append(f"\n{str_header_1}\n") - lst.extend(lst_contents) - msg = "\n".join(lst) - print(msg, file=sys.stderr) - - return snippets - - def contents(self, id_=None): - """Get snippet contents. - - If only one snippet and id no provided, infer want the - only available snippet. - - :param id_: - - Default None. snippet_co. If know there is only one snippet, - my opt to not specify - - :type id_: str | None - :returns: - - snippet contents and actual snippet_co. Possible to infer - snippet if only one and id not provided - - :rtype: tuple[str, str] | drain_swamp.snip.ReplaceResult - - .. note:: context hint - - By providing a context hint, could achieve a better guess and - have some awareness of context - - """ - id_ = sanitize_id(id_) - - snippets = self.snippets - if isinstance(snippets, ReplaceResult): - return snippets - - seq_ret = [] - # infer -- if only one and id not provided - is_infer = len(snippets) == 1 and ( - id_ is None or (isinstance(id_, str) and len(id_.strip()) == 0) - ) - if is_infer: - # snippet_co not provided cuz knew there is only one snippet - t_snip = snippets[0] - snippet_co = t_snip[0] - snippet_contents = t_snip[1] - ret = (snippet_contents, snippet_co) - self._is_infer = is_infer - else: - # matches - for t_snip in snippets: - snippet_co = t_snip[0] - snippet_contents = t_snip[1] - if snippet_co == id_: - seq_ret.append((snippet_contents, snippet_co)) - - # ReplaceResult.NO_MATCH already checked for - if len(seq_ret) == 0: - ret = ReplaceResult.NO_MATCH - else: - ret = seq_ret[0] - self._is_infer = False - - return ret diff --git a/src/drain_swamp/snip.pyi b/src/drain_swamp/snip.pyi deleted file mode 100644 index 45320cc..0000000 --- a/src/drain_swamp/snip.pyi +++ /dev/null @@ -1,59 +0,0 @@ -import re -from collections.abc import Sequence -from enum import ( - Enum, - auto, -) -from pathlib import Path -from typing import ( - Any, - ClassVar, -) - -__all__ = ( - "Snip", - "ReplaceResult", -) - -class ReplaceResult(Enum): - VALIDATE_FAIL = auto() - NO_MATCH = auto() - REPLACED = auto() - NO_CHANGE = auto() - def __eq__(self, other: object) -> bool: ... - -def check_matching_tag_count( - contents: str, - token_start: str | None = None, - token_end: str | None = None, -) -> bool: ... -def check_not_nested_or_out_of_order( - contents: str, - token_start: str | None = None, - token_end: str | None = None, -) -> bool: ... -def sanitize_id(id_: Any | None = "") -> str: ... - -class Snip: - TOKEN_START: ClassVar[str] - TOKEN_END: ClassVar[str] - PATTERN_W_ID: re.Pattern[str] - - def __init__( - self, - fname: str | Path, - ) -> None: ... - @property - def path_file(self) -> Path: ... - @path_file.setter - def path_file(self, val: Any) -> None: ... - def is_file_ok(self) -> bool: ... - @property - def is_infer(self) -> bool: ... - @property - def snippets(self) -> Sequence[tuple[str, str]] | ReplaceResult: ... - def print(self) -> Sequence[tuple[str, str]] | ReplaceResult: ... - def get_file(self) -> str: ... - def validate(self) -> bool: ... - def contents(self, id_: str | None = None) -> str | ReplaceResult: ... - def replace(self, replacement: str, id_: str | None = "") -> ReplaceResult: ... diff --git a/src/drain_swamp/snippet_pyproject_toml.py b/src/drain_swamp/snippet_pyproject_toml.py index f7d7428..9ae5e7a 100644 --- a/src/drain_swamp/snippet_pyproject_toml.py +++ b/src/drain_swamp/snippet_pyproject_toml.py @@ -45,16 +45,17 @@ import logging +from drain_swamp_snippet import ( + ReplaceResult, + Snip, +) + from .constants import ( SUFFIX_LOCKED, SUFFIX_SYMLINK, SUFFIX_UNLOCKED, g_app_name, ) -from .snip import ( - ReplaceResult, - Snip, -) SNIPPET_NO_MATCH = "In pyproject.toml, there is no snippet with snippet code {}" SNIPPET_VALIDATE_FAIL = ( @@ -88,7 +89,7 @@ def snippet_replace_suffixes(path_config, snippet_co=None): On success, None. Otherwise check for ReplaceResult.VALIDATE_FAIL and ReplaceResult.NO_MATCH - :rtype: drain_swamp.snip.ReplaceResult | None + :rtype: drain_swamp_snippet.ReplaceResult | None """ modpath = f"{g_app_name}.snippet_pyproject_toml.snippet_replace_suffixes" diff --git a/src/drain_swamp/snippet_pyproject_toml.pyi b/src/drain_swamp/snippet_pyproject_toml.pyi index d8a4e18..a9a6acf 100644 --- a/src/drain_swamp/snippet_pyproject_toml.pyi +++ b/src/drain_swamp/snippet_pyproject_toml.pyi @@ -2,7 +2,7 @@ import logging from pathlib import Path from typing import Final -from .snip import ReplaceResult +from drain_swamp_snippet import ReplaceResult SNIPPET_NO_MATCH: Final[str] # noqa: F401 SNIPPET_VALIDATE_FAIL: Final[str] # noqa: F401 diff --git a/src/drain_swamp/snippet_sphinx_conf.py b/src/drain_swamp/snippet_sphinx_conf.py index a240d14..fa21fcc 100644 --- a/src/drain_swamp/snippet_sphinx_conf.py +++ b/src/drain_swamp/snippet_sphinx_conf.py @@ -36,13 +36,14 @@ import logging import textwrap -from .constants import g_app_name -from .monkey.patch_strftime import StrFTime -from .package_metadata import PackageMetadata -from .snip import ( +from drain_swamp_snippet import ( ReplaceResult, Snip, ) + +from .constants import g_app_name +from .monkey.patch_strftime import StrFTime +from .package_metadata import PackageMetadata from .version_semantic import ( SemVersion, _path_or_cwd, @@ -324,7 +325,7 @@ def replace(self, snippet_co=None): - NO_CHANGE -- no replacement occurred - REPLACED -- replaced contents - :rtype: drain_swamp.snip.ReplaceResult + :rtype: drain_swamp_snippet.ReplaceResult """ # is_ok check avoids, TypeError contents = self._contents diff --git a/tests/_bad_files/backend_only.pyproject_toml b/tests/_bad_files/backend_only.pyproject_toml index 65f0b1c..e23836b 100644 --- a/tests/_bad_files/backend_only.pyproject_toml +++ b/tests/_bad_files/backend_only.pyproject_toml @@ -1,3 +1,11 @@ [build-system] -requires = ["setuptools>=70.0.0", "wheel", "build", "setuptools_scm>=8", "click", "pluggy"] +requires = [ + "setuptools>=70.0.0", + "wheel", + "build", + "setuptools_scm>=8", + "click", + "pluggy", + "drain-swamp-snippet", +] build-backend = "setuptools.build_meta diff --git a/tests/_bad_files/complete-version-other.pyproject_toml b/tests/_bad_files/complete-version-other.pyproject_toml index 15c9957..176fcc9 100644 --- a/tests/_bad_files/complete-version-other.pyproject_toml +++ b/tests/_bad_files/complete-version-other.pyproject_toml @@ -1,5 +1,13 @@ [build-system] -requires = ["setuptools>=70.0.0", "wheel", "build", "setuptools_scm>=8", "click", "pluggy"] +requires = [ + "setuptools>=70.0.0", + "wheel", + "build", + "setuptools_scm>=8", + "click", + "pluggy", + "drain-swamp-snippet", +] build-backend = "setuptools.build_meta" [project] diff --git a/tests/_bad_files/snippet-nested.pyproject_toml b/tests/_bad_files/snippet-nested.pyproject_toml index fa668a6..4db3646 100644 --- a/tests/_bad_files/snippet-nested.pyproject_toml +++ b/tests/_bad_files/snippet-nested.pyproject_toml @@ -1,5 +1,13 @@ [build-system] -requires = ["setuptools>=70.0.0", "wheel", "build", "setuptools_scm>=8", "click", "pluggy"] +requires = [ + "setuptools>=70.0.0", + "wheel", + "build", + "setuptools_scm>=8", + "click", + "pluggy", + "drain-swamp-snippet", +] build-backend = "setuptools.build_meta" [project] diff --git a/tests/_bad_files/static-version.pyproject_toml b/tests/_bad_files/static-version.pyproject_toml index e93fdd2..adc4e74 100644 --- a/tests/_bad_files/static-version.pyproject_toml +++ b/tests/_bad_files/static-version.pyproject_toml @@ -1,5 +1,13 @@ [build-system] -requires = ["setuptools>=70.0.0", "wheel", "build", "setuptools_scm>=8", "click", "pluggy"] +requires = [ + "setuptools>=70.0.0", + "wheel", + "build", + "setuptools_scm>=8", + "click", + "pluggy", + "drain-swamp-snippet", +] build-backend = "setuptools.build_meta" [project] diff --git a/tests/_bad_files/static_dependencies.pyproject_toml b/tests/_bad_files/static_dependencies.pyproject_toml index 3bcced5..1ab7ab0 100644 --- a/tests/_bad_files/static_dependencies.pyproject_toml +++ b/tests/_bad_files/static_dependencies.pyproject_toml @@ -1,5 +1,13 @@ [build-system] -requires = ["setuptools>=70.0.0", "wheel", "build", "setuptools_scm>=8", "click", "pluggy"] +requires = [ + "setuptools>=70.0.0", + "wheel", + "build", + "setuptools_scm>=8", + "click", + "pluggy", + "drain-swamp-snippet", +] build-backend = "setuptools.build_meta" [project] diff --git a/tests/_good_files/backend-only.pyproject_toml b/tests/_good_files/backend-only.pyproject_toml index 3123052..3f1e438 100644 --- a/tests/_good_files/backend-only.pyproject_toml +++ b/tests/_good_files/backend-only.pyproject_toml @@ -1,5 +1,13 @@ [build-system] -requires = ["setuptools>=70.0.0", "wheel", "build", "setuptools_scm>=8", "click", "pluggy"] +requires = [ + "setuptools>=70.0.0", + "wheel", + "build", + "setuptools_scm>=8", + "click", + "pluggy", + "drain-swamp-snippet", +] build-backend = "setuptools.build_meta" [project] diff --git a/tests/_good_files/backend-unsupported.pyproject_toml b/tests/_good_files/backend-unsupported.pyproject_toml index ff6bd2f..8ddd273 100644 --- a/tests/_good_files/backend-unsupported.pyproject_toml +++ b/tests/_good_files/backend-unsupported.pyproject_toml @@ -1,3 +1,11 @@ [build-system] -requires = ["setuptools>=70.0.0", "wheel", "build", "setuptools_scm>=8", "click", "pluggy"] +requires = [ + "setuptools>=70.0.0", + "wheel", + "build", + "setuptools_scm>=8", + "click", + "pluggy", + "drain-swamp-snippet", +] build-backend = "scooby-doo-loud-but-drab-color-scheme.build_meta" diff --git a/tests/_good_files/complete-lnk-files.pyproject_toml b/tests/_good_files/complete-lnk-files.pyproject_toml index 9a82041..f951780 100644 --- a/tests/_good_files/complete-lnk-files.pyproject_toml +++ b/tests/_good_files/complete-lnk-files.pyproject_toml @@ -1,5 +1,13 @@ [build-system] -requires = ["setuptools>=70.0.0", "wheel", "build", "setuptools_scm>=8", "click", "pluggy"] +requires = [ + "setuptools>=70.0.0", + "wheel", + "build", + "setuptools_scm>=8", + "click", + "pluggy", + "drain-swamp-snippet", +] build-backend = "setuptools.build_meta" [project] diff --git a/tests/_good_files/complete-manage-pip-prod-unlock.pyproject_toml b/tests/_good_files/complete-manage-pip-prod-unlock.pyproject_toml index b5d6574..fc07c71 100644 --- a/tests/_good_files/complete-manage-pip-prod-unlock.pyproject_toml +++ b/tests/_good_files/complete-manage-pip-prod-unlock.pyproject_toml @@ -1,5 +1,13 @@ [build-system] -requires = ["setuptools>=70.0.0", "wheel", "build", "setuptools_scm>=8", "click", "pluggy"] +requires = [ + "setuptools>=70.0.0", + "wheel", + "build", + "setuptools_scm>=8", + "click", + "pluggy", + "drain-swamp-snippet", +] build-backend = "setuptools.build_meta" [project] diff --git a/tests/_good_files/complete-version-txt.pyproject_toml b/tests/_good_files/complete-version-txt.pyproject_toml index be0336c..d688a67 100644 --- a/tests/_good_files/complete-version-txt.pyproject_toml +++ b/tests/_good_files/complete-version-txt.pyproject_toml @@ -1,5 +1,13 @@ [build-system] -requires = ["setuptools>=70.0.0", "wheel", "build", "setuptools_scm>=8", "click", "pluggy"] +requires = [ + "setuptools>=70.0.0", + "wheel", + "build", + "setuptools_scm>=8", + "click", + "pluggy", + "drain-swamp-snippet", +] build-backend = "setuptools.build_meta" [project] diff --git a/tests/_good_files/complete.pyproject_toml b/tests/_good_files/complete.pyproject_toml index e05c2c9..947d5d8 100644 --- a/tests/_good_files/complete.pyproject_toml +++ b/tests/_good_files/complete.pyproject_toml @@ -1,5 +1,13 @@ [build-system] -requires = ["setuptools>=70.0.0", "wheel", "build", "setuptools_scm>=8", "click", "pluggy"] +requires = [ + "setuptools>=70.0.0", + "wheel", + "build", + "setuptools_scm>=8", + "click", + "pluggy", + "drain-swamp-snippet", +] build-backend = "setuptools.build_meta" [project] diff --git a/tests/_good_files/multiple-snippets.pyproject_toml b/tests/_good_files/multiple-snippets.pyproject_toml index 592a238..c120dc2 100644 --- a/tests/_good_files/multiple-snippets.pyproject_toml +++ b/tests/_good_files/multiple-snippets.pyproject_toml @@ -1,5 +1,13 @@ [build-system] -requires = ["setuptools>=70.0.0", "wheel", "build", "setuptools_scm>=8", "click", "pluggy"] +requires = [ + "setuptools>=70.0.0", + "wheel", + "build", + "setuptools_scm>=8", + "click", + "pluggy", + "drain-swamp-snippet", +] build-backend = "setuptools.build_meta" [project] diff --git a/tests/_good_files/no_copyright.pyproject_toml b/tests/_good_files/no_copyright.pyproject_toml index 49ea667..6cae495 100644 --- a/tests/_good_files/no_copyright.pyproject_toml +++ b/tests/_good_files/no_copyright.pyproject_toml @@ -1,5 +1,13 @@ [build-system] -requires = ["setuptools>=70.0.0", "wheel", "build", "setuptools_scm>=8", "click", "pluggy"] +requires = [ + "setuptools>=70.0.0", + "wheel", + "build", + "setuptools_scm>=8", + "click", + "pluggy", + "drain-swamp-snippet", +] build-backend = "setuptools.build_meta" [project] diff --git a/tests/_good_files/no_project_name.pyproject_toml b/tests/_good_files/no_project_name.pyproject_toml index 4d962cd..129eb07 100644 --- a/tests/_good_files/no_project_name.pyproject_toml +++ b/tests/_good_files/no_project_name.pyproject_toml @@ -1,5 +1,13 @@ [build-system] -requires = ["setuptools>=70.0.0", "wheel", "build", "setuptools_scm>=8", "click", "pluggy"] +requires = [ + "setuptools>=70.0.0", + "wheel", + "build", + "setuptools_scm>=8", + "click", + "pluggy", + "drain-swamp-snippet", +] build-backend = "setuptools.build_meta" [project] diff --git a/tests/_good_files/nonsense-keys.pyproject_toml b/tests/_good_files/nonsense-keys.pyproject_toml index 10d69fe..43d7568 100644 --- a/tests/_good_files/nonsense-keys.pyproject_toml +++ b/tests/_good_files/nonsense-keys.pyproject_toml @@ -1,5 +1,13 @@ [build-system] -requires = ["setuptools>=70.0.0", "wheel", "build", "setuptools_scm>=8", "click", "pluggy"] +requires = [ + "setuptools>=70.0.0", + "wheel", + "build", + "setuptools_scm>=8", + "click", + "pluggy", + "drain-swamp-snippet", +] build-backend = "setuptools.build_meta" [project] diff --git a/tests/_good_files/requires-none.pyproject_toml b/tests/_good_files/requires-none.pyproject_toml index 4faaae1..1f6aefd 100644 --- a/tests/_good_files/requires-none.pyproject_toml +++ b/tests/_good_files/requires-none.pyproject_toml @@ -1,5 +1,13 @@ [build-system] -requires = ["setuptools>=70.0.0", "wheel", "build", "setuptools_scm>=8", "click", "pluggy"] +requires = [ + "setuptools>=70.0.0", + "wheel", + "build", + "setuptools_scm>=8", + "click", + "pluggy", + "drain-swamp-snippet", +] build-backend = "setuptools.build_meta" [project] diff --git a/tests/_good_files/thin-wrap-backend.pyproject_toml b/tests/_good_files/thin-wrap-backend.pyproject_toml index c07484a..e9d8eca 100644 --- a/tests/_good_files/thin-wrap-backend.pyproject_toml +++ b/tests/_good_files/thin-wrap-backend.pyproject_toml @@ -1,5 +1,13 @@ [build-system] -requires = ["setuptools>=70.0.0", "wheel", "build", "setuptools_scm>=8", "click", "pluggy"] +requires = [ + "setuptools>=70.0.0", + "wheel", + "build", + "setuptools_scm>=8", + "click", + "pluggy", + "drain-swamp-snippet", +] build-backend = "backend" backend-path = ["_req_links"] diff --git a/tests/_good_files/weird_copyright-2.pyproject_toml b/tests/_good_files/weird_copyright-2.pyproject_toml index ff388e4..2100586 100644 --- a/tests/_good_files/weird_copyright-2.pyproject_toml +++ b/tests/_good_files/weird_copyright-2.pyproject_toml @@ -1,5 +1,13 @@ [build-system] -requires = ["setuptools>=70.0.0", "wheel", "build", "setuptools_scm>=8", "click", "pluggy"] +requires = [ + "setuptools>=70.0.0", + "wheel", + "build", + "setuptools_scm>=8", + "click", + "pluggy", + "drain-swamp-snippet", +] build-backend = "setuptools.build_meta" [project] diff --git a/tests/_good_files/weird_copyright.pyproject_toml b/tests/_good_files/weird_copyright.pyproject_toml index 97fc7e0..47c05a6 100644 --- a/tests/_good_files/weird_copyright.pyproject_toml +++ b/tests/_good_files/weird_copyright.pyproject_toml @@ -1,5 +1,13 @@ [build-system] -requires = ["setuptools>=70.0.0", "wheel", "build", "setuptools_scm>=8", "click", "pluggy"] +requires = [ + "setuptools>=70.0.0", + "wheel", + "build", + "setuptools_scm>=8", + "click", + "pluggy", + "drain-swamp-snippet", +] build-backend = "setuptools.build_meta" [project] diff --git a/tests/_project/install_minimum.pyproject_toml b/tests/_project/install_minimum.pyproject_toml index 7b95af0..8b4f6ef 100644 --- a/tests/_project/install_minimum.pyproject_toml +++ b/tests/_project/install_minimum.pyproject_toml @@ -1,5 +1,13 @@ [build-system] -requires = ["setuptools>=70.0.0", "wheel", "build", "setuptools_scm>=8", "drain_swamp"] +requires = [ + "setuptools>=70.0.0", + "wheel", + "build", + "setuptools_scm>=8", + "click", + "pluggy", + "drain-swamp-snippet", +] build-backend = "setuptools.build_meta" [project] diff --git a/tests/test_backend_abc.py b/tests/test_backend_abc.py index d370836..7c3059c 100644 --- a/tests/test_backend_abc.py +++ b/tests/test_backend_abc.py @@ -38,6 +38,7 @@ ) import pytest +from drain_swamp_snippet import ReplaceResult from drain_swamp._run_cmd import run_cmd from drain_swamp._safe_path import ( @@ -62,7 +63,6 @@ ) from drain_swamp.exceptions import PyProjectTOMLReadError from drain_swamp.parser_in import TomlParser -from drain_swamp.snip import ReplaceResult if sys.version_info >= (3, 9): # pragma: no cover from collections.abc import Sequence diff --git a/tests/test_snip.py b/tests/test_snip.py deleted file mode 100644 index 03cd6c2..0000000 --- a/tests/test_snip.py +++ /dev/null @@ -1,645 +0,0 @@ -""" -.. moduleauthor:: Dave Faulkmore - -Unittest for module, drain_swamp.snip - -Unit test -- Module - -.. code-block:: shell - - python -m coverage run --source='drain_swamp.snip' -m pytest \ - --showlocals tests/test_snip.py && coverage report \ - --data-file=.coverage --include="**/snip.py" - -Integration test - -.. code-block:: shell - - make coverage - pytest --showlocals --cov="drain_swamp" --cov-report=term-missing \ - --cov-config=pyproject.toml tests - -""" - -import contextlib -import io -import logging -import logging.config -from pathlib import Path - -import pytest - -from drain_swamp.constants import ( - LOGGING, - g_app_name, -) -from drain_swamp.snip import ( - ReplaceResult, - Snip, - check_matching_tag_count, - check_not_nested_or_out_of_order, -) - -testdata_test_snip_harden = [ - ( - "with_id_key_no_snippet.txt", - "asdf", - "abc abc abc", - "zzzzzzzzzzzzz\nzzzzzzzzzz\nzzzzzzzzzzzzz\n", - "zzzzzzzzzzzzz\nzzzzzzzzzz\nzzzzzzzzzzzzz\n", - ReplaceResult.NO_MATCH, - ), - ( - "with_id_key_no_key.txt", - "asdf", - "abc abc abc", - "zzzzzzzzzzzzz\n# @@@ editable\nblah blah blah\n# @@@ end\nzzzzzzzzzzzzz\n", - "zzzzzzzzzzzzz\n# @@@ editable\nblah blah blah\n# @@@ end\nzzzzzzzzzzzzz\n", - ReplaceResult.NO_MATCH, - ), - ( - "with_id_no_key_key.txt", - "", - "abc abc abc", - "zzzzzzzzzzzzz\n# @@@ editable george\nblah blah blah\n# @@@ end\nzzzzzzzzzzzzz\n", - "zzzzzzzzzzzzz\n# @@@ editable george\nabc abc abc\n# @@@ end\nzzzzzzzzzzzzz\n", - ReplaceResult.REPLACED, - ), - ( - "with_id_no_change.txt", - "asdf", - "blah blah blah", - "zzzzzzzzzzzzz\n# @@@ editable asdf\nblah blah blah\n# @@@ end\nzzzzzzzzzzzzz\n", - "zzzzzzzzzzzzz\n# @@@ editable asdf\nblah blah blah\n# @@@ end\nzzzzzzzzzzzzz\n", - ReplaceResult.NO_CHANGE, - ), - ( - "with_id.txt", - "asdf", - "abc abc abc", - "zzzzzzzzzzzzz\n# @@@ editable asdf\nblah blah blah\n# @@@ end\nzzzzzzzzzzzzz\n", - "zzzzzzzzzzzzz\n# @@@ editable asdf\nabc abc abc\n# @@@ end\nzzzzzzzzzzzzz\n", - ReplaceResult.REPLACED, - ), - ( - "without_id_have_id_no_match.txt", - "george", - "abc abc abc", - "zzzzzzzzzzzzz\n# @@@ editable\nblah blah blah\n# @@@ end\nzzzzzzzzzzzzz\nzzzzzzzzzzzzz\n# @@@ editable george\nblah blah blah\n# @@@ end\nzzzzzzzzzzzzz\n", - "zzzzzzzzzzzzz\n# @@@ editable\nblah blah blah\n# @@@ end\nzzzzzzzzzzzzz\nzzzzzzzzzzzzz\n# @@@ editable george\nabc abc abc\n# @@@ end\nzzzzzzzzzzzzz\n", - ReplaceResult.REPLACED, - ), - ( - "with_id_snippet_empty.txt", - "asdf", - "abc abc abc", - "zzzzzzzzzzzzz\n# @@@ editable asdf\n\n# @@@ end\nzzzzzzzzzzzzz\n", - "zzzzzzzzzzzzz\n# @@@ editable asdf\nabc abc abc\n# @@@ end\nzzzzzzzzzzzzz\n", - ReplaceResult.REPLACED, - ), - ( - "without_id_empty_str.txt", - "", - "abc abc abc", - "zzzzzzzzzzzzz\n# @@@ editable\nblah blah blah\n# @@@ end\nzzzzzzzzzzzzz\n", - "zzzzzzzzzzzzz\n# @@@ editable\nabc abc abc\n# @@@ end\nzzzzzzzzzzzzz\n", - ReplaceResult.REPLACED, - ), - ( - "without_id_none.txt", - None, - "abc abc abc", - "zzzzzzzzzzzzz\n# @@@ editable\nblah blah blah\n# @@@ end\nzzzzzzzzzzzzz\n", - "zzzzzzzzzzzzz\n# @@@ editable\nabc abc abc\n# @@@ end\nzzzzzzzzzzzzz\n", - ReplaceResult.REPLACED, - ), - ( - "without_id_not_str.txt", - 1.12345, - "abc abc abc", - "zzzzzzzzzzzzz\n# @@@ editable\nblah blah blah\n# @@@ end\nzzzzzzzzzzzzz\n", - "zzzzzzzzzzzzz\n# @@@ editable\nabc abc abc\n# @@@ end\nzzzzzzzzzzzzz\n", - ReplaceResult.REPLACED, - ), - ( - "without_id_excess_whitespace.txt", - " ", - "abc abc abc", - "zzzzzzzzzzzzz\n# @@@ editable\nblah blah blah\n# @@@ end\nzzzzzzzzzzzzz\n", - "zzzzzzzzzzzzz\n# @@@ editable\nabc abc abc\n# @@@ end\nzzzzzzzzzzzzz\n", - ReplaceResult.REPLACED, - ), - ( - "with_id_match_2nd.txt", - "george", - "abc abc abc", - "zzzzzzzzzzzzz\n# @@@ editable asdf\nblah blah blah\n# @@@ end\nzzzzzz\n# @@@ editable george\nhey there ted\n# @@@ end\nzzzzzzz\n", - "zzzzzzzzzzzzz\n# @@@ editable asdf\nblah blah blah\n# @@@ end\nzzzzzz\n# @@@ editable george\nabc abc abc\n# @@@ end\nzzzzzzz\n", - ReplaceResult.REPLACED, - ), - ( - "with_id_match_1st.txt", - "asdf", - "abc abc abc", - "zzzzzzzzzzzzz\n# @@@ editable asdf\nblah blah blah\n# @@@ end\nzzzzzz\n# @@@ editable george\nhey there ted\n# @@@ end\nzzzzzzz\n", - "zzzzzzzzzzzzz\n# @@@ editable asdf\nabc abc abc\n# @@@ end\nzzzzzz\n# @@@ editable george\nhey there ted\n# @@@ end\nzzzzzzz\n", - ReplaceResult.REPLACED, - ), - ( - "with_id_match_1st_and_3rd.txt", - "asdf", - "abc abc abc", - "zzzzzzzzzzzzz\n# @@@ editable asdf\nblah blah blah\n# @@@ end\nzzzzzz\n# @@@ editable george\nhey there ted\n# @@@ end\nzzzzzzz\n# @@@ editable asdf\nlol lol lol\n# @@@ end\noh ok then", - "zzzzzzzzzzzzz\n# @@@ editable asdf\nabc abc abc\n# @@@ end\nzzzzzz\n# @@@ editable george\nhey there ted\n# @@@ end\nzzzzzzz\n# @@@ editable asdf\nabc abc abc\n# @@@ end\noh ok then", - ReplaceResult.REPLACED, - ), -] -ids_test_snip_harden = [ - "No snippet. Nothing to do", - "one snip. with id. key / no key", - "one snip. with id. no key / key", - "one snip. with id. no change", - "one snip. with id", - "two snippets. one with one without id. key. invalid to mix", - "one snip. with id. snippet empty", - "one snip. without id empty str", - "one snip. without id empty none", - "one snip. without id empty not a str", - "one snip. without id empty excess whitespace", - "two snips. both have id. match 2nd", - "two snips. both have id. match 1st", - "three snips. All have id. match 1st and 3rd", -] - - -@pytest.mark.parametrize( - "file_name, id_, replace_text, file_contents, expected, expected_result_status", - testdata_test_snip_harden, - ids=ids_test_snip_harden, -) -def test_snip_harden( - file_name, - id_, - replace_text, - file_contents, - expected, - expected_result_status, - tmp_path, - caplog, - file_regression, - has_logging_occurred, -): - """Test Snip.replace failure and normal usage. - - .. seealso:: - - `FileRegressionFixture.check `_ - - """ - # pytest --showlocals --log-level INFO -k "test_snip_harden" tests - # py39+ Cannot have LOGGING.loggers.root - LOGGING["loggers"][g_app_name]["propagate"] = True - logging.config.dictConfig(LOGGING) - logger = logging.getLogger(name=g_app_name) - logger.addHandler(hdlr=caplog.handler) - caplog.handler.level = logger.level - - # files/folders --> FileNotFoundError --> False - invalids = ( - str(tmp_path), # a folder - Path("/proc/tmp/george/bob/ted/todd/dan.txt"), # nonexistent file - Path("__init__.py"), # relative file (not scary normally empty) - ) - for invalid in invalids: - er_bad = Snip(invalid) - # Fails at this point, not on class construction - is_success = er_bad.replace(replace_text, id_=id_) - assert is_success is None or ReplaceResult.VALIDATE_FAIL == is_success - - # prepare - path_fname = tmp_path.joinpath(file_name) - - path_fnames = ( - path_fname, - str(path_fname), - ) - for mixed_f in path_fnames: - Path(mixed_f).touch() - Path(mixed_f).write_text("") # reset file contents - - # empty file --> ValueError - er_bad_val = Snip(mixed_f) - with pytest.raises(ValueError): - er_bad_val.get_file() - - for mixed_f in path_fnames: - path_f = Path(mixed_f) - path_f.write_text(file_contents) - er = Snip(mixed_f) - - # pass in non-str - invalids = ( - None, - 1.1234, - 1, - ) - for invalid in invalids: - with pytest.raises(TypeError): - er.replace(invalid, id_=id_) - - # normal call - is_success = er.replace(replace_text, id_=id_) - if is_success is None or is_success == ReplaceResult.VALIDATE_FAIL: - # issue with file or one or more validation checks failed - pass - else: - assert is_success == expected_result_status - actual = er.get_file() - assert actual == expected - - assert has_logging_occurred(caplog) - - file_regression.check(expected, extension=".txt", binary=False) - - -testdata_snip_properties = ( - ( - None, - pytest.raises(TypeError), - ), - ( - 1.12345, - pytest.raises(TypeError), - ), - ( - 1, - pytest.raises(TypeError), - ), -) -ids_snip_properties = ( - "unsupported None", - "unsupported type float", - "unsupported type int", -) - - -@pytest.mark.parametrize( - "path_f, expectation", - testdata_snip_properties, - ids=ids_snip_properties, -) -def test_snip_properties(path_f, expectation): - """Test Snip properties.""" - # pytest --showlocals --log-level INFO -k "test_properties" tests - with expectation: - Snip(path_f) - - -@pytest.mark.parametrize( - "file_name, id_, replace_text, file_contents, expected, expected_result_status", - testdata_test_snip_harden, - ids=ids_test_snip_harden, -) -def test_checks_normal_usage( - file_name, - id_, - replace_text, - file_contents, - expected, - expected_result_status, - caplog, -): - """Snippet validity checks.""" - # pytest --showlocals --log-level INFO -k "test_checks_normal_usage" tests - LOGGING["loggers"][g_app_name]["propagate"] = True - logging.config.dictConfig(LOGGING) - logger = logging.getLogger(name=g_app_name) - logger.addHandler(hdlr=caplog.handler) - caplog.handler.level = logger.level - - assert check_matching_tag_count( - file_contents, - Snip.TOKEN_START, - Snip.TOKEN_END, - ) - - # check_matching_tag_count must occur before this check - # arg validation -- invalids - invalids = ( - None, - "", - " ", - 1.12345, - ) - test_data = [] - a_ = file_contents - b_ = Snip.TOKEN_START - c_ = Snip.TOKEN_END - for invalid_a in invalids: - test_data.append((invalid_a, b_, c_)) - for invalid_b in invalids: - test_data.append((a_, invalid_b, c_)) - for invalid_c in invalids: - test_data.append((a_, b_, invalid_c)) - for t_args in test_data: - assert check_not_nested_or_out_of_order(*t_args) is False - - # normal usage - assert check_not_nested_or_out_of_order( - file_contents, - Snip.TOKEN_START, - Snip.TOKEN_END, - ) - - -@pytest.mark.parametrize( - "file_name, id_, replace_text, file_contents, expected, expected_result_status", - testdata_test_snip_harden, - ids=ids_test_snip_harden, -) -def test_checks_bad_input( - file_name, - id_, - replace_text, - file_contents, - expected, - expected_result_status, - caplog, -): - """Snippet validity checks bad input.""" - # pytest --showlocals --log-level INFO -k "test_checks_bad_input" tests - LOGGING["loggers"][g_app_name]["propagate"] = True - logging.config.dictConfig(LOGGING) - logger = logging.getLogger(name=g_app_name) - logger.addHandler(hdlr=caplog.handler) - caplog.handler.level = logger.level - - # prepare test data - invalids = ( - None, - "", - " ", - 1.12345, - ) - test_data_checks_bad_input = [] - a_ = file_contents - b_ = Snip.TOKEN_START - c_ = Snip.TOKEN_END - for invalid_a in invalids: - test_data_checks_bad_input.append((invalid_a, b_, c_)) - for invalid_b in invalids: - test_data_checks_bad_input.append((a_, invalid_b, c_)) - for invalid_c in invalids: - test_data_checks_bad_input.append((a_, b_, invalid_c)) - - # check_matching_tag_count - for t_args in test_data_checks_bad_input: - assert check_matching_tag_count(*t_args) is False - - # check_not_nested_or_out_of_order - for t_args in test_data_checks_bad_input: - assert check_not_nested_or_out_of_order(*t_args) is False - - -SNIPS_BAD = list(Path(__file__).parent.joinpath("_bad_snips").glob("*.txt")) - - -@pytest.mark.parametrize( - "path", - SNIPS_BAD, - ids=[path.name.rsplit(".", 1)[0] for path in SNIPS_BAD], -) -def test_check_snips_bad(path): - """Snips gone wrong.""" - # pytest --showlocals --log-level INFO -k "test_check_snips_bad" tests - - file_contents = path.read_text() - invalids = [ - (file_contents, Snip.TOKEN_START, Snip.TOKEN_END), - ] - - for t_args in invalids: - # check_matching_tag_count ... pass - assert check_matching_tag_count(*t_args) - - # check_not_nested_or_out_of_order ... fail - assert not check_not_nested_or_out_of_order(*t_args) - - # bad files should fail validation - snip = Snip(path) - assert snip.validate() is False - - -@pytest.mark.parametrize( - "file_name, id_, replace_text, file_contents, expected, expected_result_status", - testdata_test_snip_harden, - ids=ids_test_snip_harden, -) -def test_snip_validate( - file_name, - id_, - replace_text, - file_contents, - expected, - expected_result_status, - caplog, - tmp_path, -): - """Test Snip.validate.""" - # pytest --showlocals --log-level INFO -k "test_snip_validate" tests - LOGGING["loggers"][g_app_name]["propagate"] = True - logging.config.dictConfig(LOGGING) - logger = logging.getLogger(name=g_app_name) - logger.addHandler(hdlr=caplog.handler) - caplog.handler.level = logger.level - - # prepare - path_fname = tmp_path.joinpath(file_name) - - path_fnames = ( - path_fname, - str(path_fname), - ) - for mixed_f in path_fnames: - Path(mixed_f).touch() - Path(mixed_f).write_text("") # reset file contents - - # empty file --> ValueError --> False - snip_empty = Snip(mixed_f) - assert snip_empty.validate() is False - - Path(mixed_f).write_text(file_contents) - - # good - snip = Snip(mixed_f) - is_valid = snip.validate() - assert is_valid is True - - # invalid files/folders --> FileNotFoundError --> False - invalids = ( - str(tmp_path), # a folder - Path("/proc/tmp/george/bob/ted/todd/dan.txt"), # nonexistent file - Path("__init__.py"), # relative file (not scary normally empty) - ) - for invalid in invalids: - snip_bad = Snip(invalid) - # Fails at this point, not on class construction - is_valid = snip_bad.validate() - assert is_valid is False - - -testdata_snip_contents = [ - ( - "with_id_key_no_snippet.txt", - "asdf", - "zzzzzzzzzzzzz\nzzzzzzzzzz\nzzzzzzzzzzzzz\n", - ReplaceResult.NO_MATCH, - ), - ( - "with_id_key_no_key.txt", - "asdf", - "zzzzzzzzzzzzz\n# @@@ editable\nblah blah blah\n# @@@ end\nzzzzzzzzzzzzz\n", - ReplaceResult.NO_MATCH, - ), - ( - "with_id_no_key_key.txt", - "", - "zzzzzzzzzzzzz\n# @@@ editable george\nblah blah blah\n# @@@ end\nzzzzzzzzzzzzz\n", - "blah blah blah", - ), - ( - "with_id.txt", - "asdf", - "zzzzzzzzzzzzz\n# @@@ editable asdf\nblah blah blah\n# @@@ end\nzzzzzzzzzzzzz\n", - "blah blah blah", - ), - ( - "with_id_snippet_empty.txt", - "asdf", - "zzzzzzzzzzzzz\n# @@@ editable asdf\n\n# @@@ end\nzzzzzzzzzzzzz\n", - "", - ), - ( - "without_id_empty_str.txt", - "", - "zzzzzzzzzzzzz\n# @@@ editable\nblah blah blah\n# @@@ end\nzzzzzzzzzzzzz\n", - "blah blah blah", - ), - ( - "without_id_none.txt", - None, - "zzzzzzzzzzzzz\n# @@@ editable\nblah blah blah\n# @@@ end\nzzzzzzzzzzzzz\n", - "blah blah blah", - ), - ( - "without_id_not_str.txt", - 1.12345, - "zzzzzzzzzzzzz\n# @@@ editable\nblah blah blah\n# @@@ end\nzzzzzzzzzzzzz\n", - "blah blah blah", - ), - ( - "without_id_excess_whitespace.txt", - " ", - "zzzzzzzzzzzzz\n# @@@ editable\nblah blah blah\n# @@@ end\nzzzzzzzzzzzzz\n", - "blah blah blah", - ), - ( - "with_id_match_2nd.txt", - "george", - "zzzzzzzzzzzzz\n# @@@ editable asdf\nblah blah blah\n# @@@ end\nzzzzzz\n# @@@ editable george\nhey there ted\n# @@@ end\nzzzzzzz\n", - "hey there ted", - ), - ( - "with_id_match_1st.txt", - "asdf", - "zzzzzzzzzzzzz\n# @@@ editable asdf\nblah blah blah\n# @@@ end\nzzzzzz\n# @@@ editable george\nhey there ted\n# @@@ end\nzzzzzzz\n", - "blah blah blah", - ), - ( - "with_id_match_1st_and_3rd.txt", - "asdf", - "zzzzzzzzzzzzz\n# @@@ editable asdf\nblah blah blah\n# @@@ end\nzzzzzz\n# @@@ editable george\nhey there ted\n# @@@ end\nzzzzzzz\n# @@@ editable asdf\nlol lol lol\n# @@@ end\noh ok then", - "blah blah blah", - ), -] -ids_snip_contents = [ - "No snippets. Nothing to do", - "one snip. with id. No key", - "one snip. with id. No key. Infers key", - "one snip. with id", - "one snip. with id. snippet empty", - "one snip. without id empty str. Infers key", - "one snip. without id empty none/ Infers key", - "one snip. without id empty not a str. Infers key", - "one snip. without id empty excess whitespace. Infers key", - "two snips. both have id. match 2nd", - "two snips. both have id. match 1st", - "three snips. All have id. match 1st and 3rd", -] - - -@pytest.mark.parametrize( - "file_name, id_, file_contents, expected", - testdata_snip_contents, - ids=ids_snip_contents, -) -def test_snip_contents( - file_name, - id_, - file_contents, - expected, - tmp_path, - prepare_folders_files, - caplog, - has_logging_occurred, -): - """Test snippet algo.""" - # pytest --showlocals --log-level INFO -k "test_snip_contents" tests - # pytest --showlocals --log-level INFO tests/test_snip.py::test_snip_contents["No snippets. Nothing to do"] - LOGGING["loggers"][g_app_name]["propagate"] = True - logging.config.dictConfig(LOGGING) - logger = logging.getLogger(name=g_app_name) - logger.addHandler(hdlr=caplog.handler) - caplog.handler.level = logger.level - - path_abs = tmp_path / file_name - snip = Snip(path_abs) - - # No preparation -- snip.contents - t_actual = snip.contents(id_=id_) - assert t_actual == ReplaceResult.VALIDATE_FAIL - - # No preparation -- snip.print - with contextlib.redirect_stderr(io.StringIO()): - snippets = snip.print() - assert snippets == ReplaceResult.VALIDATE_FAIL - - # prepare - seq_create_these = (file_name,) - prepare_folders_files(seq_create_these, tmp_path) - path_abs.write_text(file_contents) - - # act -- snip.print - # same return value as snip.snippet - with contextlib.redirect_stderr(io.StringIO()) as f_1: - snippets = snip.print() - msg = f_1.getvalue() - - if isinstance(snippets, ReplaceResult): - assert snippets == ReplaceResult.NO_MATCH - # logger.info(f"No snippets: {snippets}\n{msg}") - pass - else: - # Block of human readable beautiful prose was printed - assert len(msg) != 0 - - # act -- snip.contents - t_actual = snip.contents(id_=id_) - - assert has_logging_occurred(caplog) - - if isinstance(t_actual, ReplaceResult): - assert t_actual == ReplaceResult.NO_MATCH - else: - # id_ might have been inferred - assert t_actual[0] == expected