diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a0173a8..7631473 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,7 +6,6 @@ on: jobs: build: - runs-on: ubuntu-latest strategy: matrix: @@ -28,14 +27,16 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install ".[dev]" - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + flake8 jsonref/ tests/ --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + flake8 jsonref/ tests/ --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics --ignore=E731,E741,W503 + - name: Test whether code is formatted with black + run: | + black --check jsonref/ tests/ - name: Test with pytest run: | - pytest tests.py + pytest tests/ diff --git a/jsonref.py b/jsonref/__init__.py similarity index 99% rename from jsonref.py rename to jsonref/__init__.py index 10451a8..a400c5f 100644 --- a/jsonref.py +++ b/jsonref/__init__.py @@ -5,6 +5,8 @@ from urllib import parse as urlparse from urllib.parse import unquote from urllib.request import urlopen +from . import proxytypes # noqa: F401 +from .proxytypes import LazyProxy try: # If requests >=1.0 is available, we will use it @@ -15,8 +17,6 @@ except ImportError: requests = None -from proxytypes import LazyProxy - __version__ = "1.1.0" @@ -350,7 +350,7 @@ def _replace_refs( merge_props, store, path, - recursing + recursing, ): base_uri, frag = urlparse.urldefrag(base_uri) store_uri = None # If this does not get set, we won't store the result @@ -424,7 +424,7 @@ def load( merge_props=False, proxies=True, lazy_load=True, - **kwargs + **kwargs, ): """ Drop in replacement for :func:`json.load`, where JSON references are @@ -461,7 +461,7 @@ def loads( merge_props=False, proxies=True, lazy_load=True, - **kwargs + **kwargs, ): """ Drop in replacement for :func:`json.loads`, where JSON references are diff --git a/proxytypes.py b/jsonref/proxytypes.py similarity index 100% rename from proxytypes.py rename to jsonref/proxytypes.py diff --git a/pdm.lock b/pdm.lock new file mode 100644 index 0000000..f75049d --- /dev/null +++ b/pdm.lock @@ -0,0 +1,330 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default", "dev"] +strategy = ["cross_platform", "inherit_metadata"] +lock_version = "4.4.1" +content_hash = "sha256:24b61af2b4424e099e0b7f92a0b8e8474ea722bceb462232a84f1ab56b02783d" + +[[package]] +name = "black" +version = "23.3.0" +requires_python = ">=3.7" +summary = "The uncompromising code formatter." +groups = ["dev"] +dependencies = [ + "click>=8.0.0", + "mypy-extensions>=0.4.3", + "packaging>=22.0", + "pathspec>=0.9.0", + "platformdirs>=2", + "tomli>=1.1.0; python_version < \"3.11\"", + "typed-ast>=1.4.2; python_version < \"3.8\" and implementation_name == \"cpython\"", + "typing-extensions>=3.10.0.0; python_version < \"3.10\"", +] +files = [ + {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, + {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, + {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, + {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"}, + {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"}, + {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"}, + {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"}, + {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"}, + {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"}, + {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"}, + {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"}, + {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, + {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, + {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"}, + {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, + {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, +] + +[[package]] +name = "click" +version = "8.1.7" +requires_python = ">=3.7" +summary = "Composable command line interface toolkit" +groups = ["dev"] +dependencies = [ + "colorama; platform_system == \"Windows\"", + "importlib-metadata; python_version < \"3.8\"", +] +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Cross-platform colored terminal text." +groups = ["dev"] +marker = "sys_platform == \"win32\" or platform_system == \"Windows\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.0" +requires_python = ">=3.7" +summary = "Backport of PEP 654 (exception groups)" +groups = ["dev"] +marker = "python_version < \"3.11\"" +files = [ + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, +] + +[[package]] +name = "flake8" +version = "5.0.4" +requires_python = ">=3.6.1" +summary = "the modular source code checker: pep8 pyflakes and co" +groups = ["dev"] +dependencies = [ + "importlib-metadata<4.3,>=1.1.0; python_version < \"3.8\"", + "mccabe<0.8.0,>=0.7.0", + "pycodestyle<2.10.0,>=2.9.0", + "pyflakes<2.6.0,>=2.5.0", +] +files = [ + {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, + {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, +] + +[[package]] +name = "importlib-metadata" +version = "4.2.0" +requires_python = ">=3.6" +summary = "Read metadata from Python packages" +groups = ["dev"] +marker = "python_version < \"3.8\"" +dependencies = [ + "typing-extensions>=3.6.4; python_version < \"3.8\"", + "zipp>=0.5", +] +files = [ + {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, + {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +requires_python = ">=3.7" +summary = "brain-dead simple config-ini parsing" +groups = ["dev"] +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +requires_python = ">=3.6" +summary = "McCabe checker, plugin for flake8" +groups = ["dev"] +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +requires_python = ">=3.5" +summary = "Type system extensions for programs checked with the mypy type checker." +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "23.2" +requires_python = ">=3.7" +summary = "Core utilities for Python packages" +groups = ["dev"] +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pathspec" +version = "0.11.2" +requires_python = ">=3.7" +summary = "Utility library for gitignore style pattern matching of file paths." +groups = ["dev"] +files = [ + {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, + {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, +] + +[[package]] +name = "platformdirs" +version = "4.0.0" +requires_python = ">=3.7" +summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +groups = ["dev"] +dependencies = [ + "typing-extensions>=4.7.1; python_version < \"3.8\"", +] +files = [ + {file = "platformdirs-4.0.0-py3-none-any.whl", hash = "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b"}, + {file = "platformdirs-4.0.0.tar.gz", hash = "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731"}, +] + +[[package]] +name = "pluggy" +version = "1.2.0" +requires_python = ">=3.7" +summary = "plugin and hook calling mechanisms for python" +groups = ["dev"] +dependencies = [ + "importlib-metadata>=0.12; python_version < \"3.8\"", +] +files = [ + {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, +] + +[[package]] +name = "pycodestyle" +version = "2.9.1" +requires_python = ">=3.6" +summary = "Python style guide checker" +groups = ["dev"] +files = [ + {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, + {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, +] + +[[package]] +name = "pyflakes" +version = "2.5.0" +requires_python = ">=3.6" +summary = "passive checker of Python programs" +groups = ["dev"] +files = [ + {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, + {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, +] + +[[package]] +name = "pytest" +version = "7.4.4" +requires_python = ">=3.7" +summary = "pytest: simple powerful testing with Python" +groups = ["dev"] +dependencies = [ + "colorama; sys_platform == \"win32\"", + "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", + "importlib-metadata>=0.12; python_version < \"3.8\"", + "iniconfig", + "packaging", + "pluggy<2.0,>=0.12", + "tomli>=1.0.0; python_version < \"3.11\"", +] +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +requires_python = ">=3.7" +summary = "A lil' TOML parser" +groups = ["dev"] +marker = "python_version < \"3.11\"" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "typed-ast" +version = "1.5.5" +requires_python = ">=3.6" +summary = "a fork of Python 2 and 3 ast modules with type comment support" +groups = ["dev"] +marker = "python_version < \"3.8\" and implementation_name == \"cpython\"" +files = [ + {file = "typed_ast-1.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b"}, + {file = "typed_ast-1.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686"}, + {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597fc66b4162f959ee6a96b978c0435bd63791e31e4f410622d19f1686d5e769"}, + {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d41b7a686ce653e06c2609075d397ebd5b969d821b9797d029fccd71fdec8e04"}, + {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5fe83a9a44c4ce67c796a1b466c270c1272e176603d5e06f6afbc101a572859d"}, + {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d5c0c112a74c0e5db2c75882a0adf3133adedcdbfd8cf7c9d6ed77365ab90a1d"}, + {file = "typed_ast-1.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:e1a976ed4cc2d71bb073e1b2a250892a6e968ff02aa14c1f40eba4f365ffec02"}, + {file = "typed_ast-1.5.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c631da9710271cb67b08bd3f3813b7af7f4c69c319b75475436fcab8c3d21bee"}, + {file = "typed_ast-1.5.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b445c2abfecab89a932b20bd8261488d574591173d07827c1eda32c457358b18"}, + {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc95ffaaab2be3b25eb938779e43f513e0e538a84dd14a5d844b8f2932593d88"}, + {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61443214d9b4c660dcf4b5307f15c12cb30bdfe9588ce6158f4a005baeb167b2"}, + {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6eb936d107e4d474940469e8ec5b380c9b329b5f08b78282d46baeebd3692dc9"}, + {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e48bf27022897577d8479eaed64701ecaf0467182448bd95759883300ca818c8"}, + {file = "typed_ast-1.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:83509f9324011c9a39faaef0922c6f720f9623afe3fe220b6d0b15638247206b"}, + {file = "typed_ast-1.5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2188bc33d85951ea4ddad55d2b35598b2709d122c11c75cffd529fbc9965508e"}, + {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0635900d16ae133cab3b26c607586131269f88266954eb04ec31535c9a12ef1e"}, + {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57bfc3cf35a0f2fdf0a88a3044aafaec1d2f24d8ae8cd87c4f58d615fb5b6311"}, + {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fe58ef6a764de7b4b36edfc8592641f56e69b7163bba9f9c8089838ee596bfb2"}, + {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d09d930c2d1d621f717bb217bf1fe2584616febb5138d9b3e8cdd26506c3f6d4"}, + {file = "typed_ast-1.5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:d40c10326893ecab8a80a53039164a224984339b2c32a6baf55ecbd5b1df6431"}, + {file = "typed_ast-1.5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fd946abf3c31fb50eee07451a6aedbfff912fcd13cf357363f5b4e834cc5e71a"}, + {file = "typed_ast-1.5.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ed4a1a42df8a3dfb6b40c3d2de109e935949f2f66b19703eafade03173f8f437"}, + {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:045f9930a1550d9352464e5149710d56a2aed23a2ffe78946478f7b5416f1ede"}, + {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:381eed9c95484ceef5ced626355fdc0765ab51d8553fec08661dce654a935db4"}, + {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bfd39a41c0ef6f31684daff53befddae608f9daf6957140228a08e51f312d7e6"}, + {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8c524eb3024edcc04e288db9541fe1f438f82d281e591c548903d5b77ad1ddd4"}, + {file = "typed_ast-1.5.5-cp38-cp38-win_amd64.whl", hash = "sha256:7f58fabdde8dcbe764cef5e1a7fcb440f2463c1bbbec1cf2a86ca7bc1f95184b"}, + {file = "typed_ast-1.5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:042eb665ff6bf020dd2243307d11ed626306b82812aba21836096d229fdc6a10"}, + {file = "typed_ast-1.5.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:622e4a006472b05cf6ef7f9f2636edc51bda670b7bbffa18d26b255269d3d814"}, + {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1efebbbf4604ad1283e963e8915daa240cb4bf5067053cf2f0baadc4d4fb51b8"}, + {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0aefdd66f1784c58f65b502b6cf8b121544680456d1cebbd300c2c813899274"}, + {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:48074261a842acf825af1968cd912f6f21357316080ebaca5f19abbb11690c8a"}, + {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:429ae404f69dc94b9361bb62291885894b7c6fb4640d561179548c849f8492ba"}, + {file = "typed_ast-1.5.5-cp39-cp39-win_amd64.whl", hash = "sha256:335f22ccb244da2b5c296e6f96b06ee9bed46526db0de38d2f0e5a6597b81155"}, + {file = "typed_ast-1.5.5.tar.gz", hash = "sha256:94282f7a354f36ef5dbce0ef3467ebf6a258e370ab33d5b40c249fa996e590dd"}, +] + +[[package]] +name = "typing-extensions" +version = "4.7.1" +requires_python = ">=3.7" +summary = "Backported and Experimental Type Hints for Python 3.7+" +groups = ["dev"] +marker = "python_version < \"3.10\"" +files = [ + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, +] + +[[package]] +name = "zipp" +version = "3.15.0" +requires_python = ">=3.7" +summary = "Backport of pathlib-compatible object wrapper for zip files" +groups = ["dev"] +marker = "python_version < \"3.8\"" +files = [ + {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, + {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, +] diff --git a/pyproject.toml b/pyproject.toml index d39f81a..7295a82 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,8 @@ [project] name = "jsonref" description = "jsonref is a library for automatic dereferencing of JSON Reference objects for Python." -authors = [ - {name = "Chase Sterling", email = "chase.sterling@gmail.com"}, -] -license = {text = "MIT"} +authors = [{ name = "Chase Sterling", email = "chase.sterling@gmail.com" }] +license = { text = "MIT" } readme = "README.md" dynamic = ["version"] requires-python = ">=3.7" @@ -14,13 +12,15 @@ dependencies = [] repository = "https://github.com/gazpachoking/jsonref" documentation = "https://jsonref.readthedocs.io/en/latest/" -[tool.pdm.dev-dependencies] -test = ["pytest>=7.1.3"] +[project.optional-dependencies] +dev = ["pytest>=7.1.3", "flake8>=5.0.4", "black>=23.3.0"] [tool.pdm] -version = { source = "file", path = "jsonref.py" } +version = { source = "file", path = "jsonref/__init__.py" } + [tool.pdm.build] -includes = ["jsonref.py", "proxytypes.py"] +includes = ["jsonref"] +excludes = ["tests", "docs"] [build-system] requires = ["pdm-backend"] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..d1824cb --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,34 @@ +from jsonref import load, loads, dump, dumps + + +class TestApi(object): + def test_loads(self): + json = """{"a": 1, "b": {"$ref": "#/a"}}""" + assert loads(json) == {"a": 1, "b": 1} + + def test_loads_kwargs(self): + json = """{"a": 5.5, "b": {"$ref": "#/a"}}""" + loaded = loads(json, parse_float=lambda x: int(float(x))) + assert loaded["a"] == loaded["b"] == 5 + + def test_load(self, tmpdir): + json = """{"a": 1, "b": {"$ref": "#/a"}}""" + tmpdir.join("in.json").write(json) + assert load(tmpdir.join("in.json")) == {"a": 1, "b": 1} + + def test_dumps(self): + json = """[1, 2, {"$ref": "#/0"}, 3]""" + loaded = loads(json) + # The string version should load the reference + assert str(loaded) == "[1, 2, 1, 3]" + # Our dump function should write the original reference + assert dumps(loaded) == json + + def test_dump(self, tmpdir): + json = """[1, 2, {"$ref": "#/0"}, 3]""" + loaded = loads(json) + # The string version should load the reference + assert str(loaded) == "[1, 2, 1, 3]" + dump(loaded, tmpdir.join("out.json")) + # Our dump function should write the original reference + assert tmpdir.join("out.json").read() == json diff --git a/tests/test_json_loader.py b/tests/test_json_loader.py new file mode 100644 index 0000000..dae4448 --- /dev/null +++ b/tests/test_json_loader.py @@ -0,0 +1,28 @@ +import json +from unittest import mock +from jsonref import jsonloader + + +class TestJsonLoader(object): + def test_it_retrieves_refs_via_requests(self): + ref = "http://bar" + data = {"baz": 12} + + with mock.patch("jsonref.requests") as requests: + requests.get.return_value.json.return_value = data + result = jsonloader(ref) + assert result == data + requests.get.assert_called_once_with("http://bar") + + def test_it_retrieves_refs_via_urlopen(self): + ref = "http://bar" + data = {"baz": 12} + + with mock.patch("jsonref.requests", None): + with mock.patch("jsonref.urlopen") as urlopen: + urlopen.return_value.__enter__.return_value.read.return_value = ( + json.dumps(data).encode("utf8") + ) + result = jsonloader(ref) + assert result == data + urlopen.assert_called_once_with("http://bar") diff --git a/tests.py b/tests/test_jsonref.py similarity index 54% rename from tests.py rename to tests/test_jsonref.py index 1cb9c94..dd67f10 100644 --- a/tests.py +++ b/tests/test_jsonref.py @@ -1,24 +1,14 @@ import functools import itertools -import json -import operator -from copy import deepcopy from unittest import mock - import pytest from jsonref import ( JsonRef, JsonRefError, _walk_refs, - dump, - dumps, - jsonloader, - load, - loads, replace_refs, ) -from proxytypes import CallbackProxy, LazyProxy, Proxy, notproxied def cmp(a, b): @@ -122,7 +112,9 @@ def test_refs_inside_extra_props(self, parametrized_replace_refs): }, "b.json": {"ba": {"a": 1}, "bb": {"b": 2}}, } - result = parametrized_replace_refs(docs["a.json"], loader=docs.get, merge_props=True) + result = parametrized_replace_refs( + docs["a.json"], loader=docs.get, merge_props=True + ) assert result == {"file": "a", "b": {"a": 1, "extra": {"b": 2}}} def test_recursive_extra(self, parametrized_replace_refs): @@ -171,7 +163,7 @@ def test_no_lazy_load(self): } # Error should raise straight away without lazy loading with pytest.raises(JsonRefError): - result = replace_refs(json, lazy_load=False) + replace_refs(json, lazy_load=False) def test_no_lazy_load_recursive(self): json = { @@ -180,7 +172,7 @@ def test_no_lazy_load_recursive(self): } # If resolution happens too early, the recursion won't work # Make sure we don't break recursion when we aren't being lazy - result = replace_refs(json, lazy_load=False) + replace_refs(json, lazy_load=False) def test_proxies(self): json = { @@ -368,375 +360,3 @@ def test_cache_loader_results(self, parametrized_replace_refs): result = parametrized_replace_refs(json, loader=loader) assert result == {"a": 1234, "b": 1234} loader.assert_called_once_with("mock://aoeu") - - -class TestJsonRefErrors(object): - def test_basic_error_properties(self): - json = [{"$ref": "#/x"}] - result = replace_refs(json) - with pytest.raises(JsonRefError) as excinfo: - result[0].__subject__ - e = excinfo.value - assert e.reference == json[0] - assert e.uri == "#/x" - assert e.base_uri == "" - assert e.path == (0,) - assert type(e.cause) == TypeError - - def test_nested_refs(self): - json = { - "a": {"$ref": "#/b"}, - "b": {"$ref": "#/c"}, - "c": {"$ref": "#/foo"}, - } - result = replace_refs(json) - with pytest.raises(JsonRefError) as excinfo: - print(result["a"]) - e = excinfo.value - assert e.path == ("c",) - - -class TestApi(object): - def test_loads(self): - json = """{"a": 1, "b": {"$ref": "#/a"}}""" - assert loads(json) == {"a": 1, "b": 1} - - def test_loads_kwargs(self): - json = """{"a": 5.5, "b": {"$ref": "#/a"}}""" - loaded = loads(json, parse_float=lambda x: int(float(x))) - assert loaded["a"] == loaded["b"] == 5 - - def test_load(self, tmpdir): - json = """{"a": 1, "b": {"$ref": "#/a"}}""" - tmpdir.join("in.json").write(json) - assert load(tmpdir.join("in.json")) == {"a": 1, "b": 1} - - def test_dumps(self): - json = """[1, 2, {"$ref": "#/0"}, 3]""" - loaded = loads(json) - # The string version should load the reference - assert str(loaded) == "[1, 2, 1, 3]" - # Our dump function should write the original reference - assert dumps(loaded) == json - - def test_dump(self, tmpdir): - json = """[1, 2, {"$ref": "#/0"}, 3]""" - loaded = loads(json) - # The string version should load the reference - assert str(loaded) == "[1, 2, 1, 3]" - dump(loaded, tmpdir.join("out.json")) - # Our dump function should write the original reference - assert tmpdir.join("out.json").read() == json - - -class TestJsonLoader(object): - def test_it_retrieves_refs_via_requests(self): - ref = "http://bar" - data = {"baz": 12} - - with mock.patch("jsonref.requests") as requests: - requests.get.return_value.json.return_value = data - result = jsonloader(ref) - assert result == data - requests.get.assert_called_once_with("http://bar") - - def test_it_retrieves_refs_via_urlopen(self): - ref = "http://bar" - data = {"baz": 12} - - with mock.patch("jsonref.requests", None): - with mock.patch("jsonref.urlopen") as urlopen: - urlopen.return_value.__enter__.return_value.read.return_value = ( - json.dumps(data).encode("utf8") - ) - result = jsonloader(ref) - assert result == data - urlopen.assert_called_once_with("http://bar") - - -_unset = object() - - -class TestProxies(object): - @pytest.fixture( - scope="class", autouse=True, params=["Proxy", "CallbackProxy", "LazyProxy"] - ) - def make_proxify(self, request): - param = request.param - - def proxify(self, val): - c = deepcopy(val) - if param == "Proxy": - return Proxy(c) - globals().get(param) - return globals().get(param)(lambda: c) - - request.cls.proxify = proxify - - def check_func(self, func, value, other=_unset): - """ - Checks func works the same with `value` as when `value` is proxied. - - """ - - p = self.proxify(value) - args = [] - if other is not _unset: - args = [other] - try: - result = func(value, *args) - except Exception as e: - with pytest.raises(type(e)): - func(p, *args) - else: - assert func(p, *args) == result - # If this func takes two arguments, try them reversed as well - if other is not _unset: - try: - result = func(other, value) - except Exception as e: - with pytest.raises(type(e)): - func(other, p) - else: - assert func(other, p) == result - - def check_integer(self, v): - for op in ( - operator.and_, - operator.or_, - operator.xor, - operator.iand, - operator.ior, - operator.ixor, - ): - self.check_func(op, v, 0b10101) - for op in ( - operator.lshift, - operator.rshift, - operator.ilshift, - operator.irshift, - ): - self.check_func(op, v, 3) - for op in (operator.invert, hex, oct): - self.check_func(op, v) - - self.check_numeric(v) - - def check_numeric(self, v): - for op in (operator.pos, operator.neg, abs, int, float, hash, complex): - self.check_func(op, v) - - for other in (5, 13.7): # Check against both an int and a float - for op in ( - # Math - operator.mul, - operator.pow, - operator.add, - operator.sub, - operator.truediv, - operator.floordiv, - operator.mod, - divmod, - # In-place - operator.imul, - operator.ipow, - operator.iadd, - operator.isub, - operator.itruediv, - operator.ifloordiv, - operator.imod, - # Comparison - operator.lt, - operator.le, - operator.gt, - operator.ge, - operator.eq, - operator.ne, - cmp, - ): - self.check_func(op, v, other) - - self.check_basics(v) - - def check_list(self, v): - for i in range(len(v)): - for arg in (i, slice(i), slice(None, i), slice(i, None, -1)): - self.check_func(operator.getitem, v, arg) - self.check_container(v) - - p = self.proxify(v) - c = list(v) - - p[1:1] = [23] - c[1:1] = [23] - assert p == c - - p.insert(1, 0) - c.insert(1, 0) - assert p == c - - p += [4] - c += [4] - assert p == c - - del p[::2] - del c[::2] - assert p == c - - def check_container(self, v): - for op in (list, set, len, sorted, lambda x: list(iter(x))): - self.check_func(op, v) - self.check_basics(v) - - def check_basics(self, v): - for f in bool, repr, str: - self.check_func(f, v) - - def test_numbers(self): - for i in range(20): - self.check_integer(i) - - f = -40 - while f <= 20.0: - self.check_numeric(f) - f += 2.25 - - def test_lists(self): - for l in [1, 2], [3, 42, 59], [99, 23, 55], ["a", "b", 1.4, 17.3, -3, 42]: - self.check_list(l) - - def test_dicts(self): - for d in ({"a": 3, 4: 2, 1.5: "b"}, {}, {"": ""}): - for op in ( - sorted, - set, - len, - lambda x: sorted(iter(x)), - operator.methodcaller("get", "a"), - ): - self.check_func(op, d) - - p = self.proxify(d) - # Use sets to make sure order doesn't matter - assert set(p.items()) == set(d.items()) - assert set(p.keys()) == set(d.keys()) - assert set(p.values()) == set(d.values()) - - self.check_basics(d) - - def test_immutable(self): - a = self.proxify(3) - b = a - b += 3 - assert a == 3 - assert b == 6 - - def test_mutable(self): - a = self.proxify([0]) - b = a - b += [1] - assert a == [0, 1] - assert b == [0, 1] - - def test_attributes(self): - class C(object): - def __init__(self): - self.attribute = "value" - - v = C() - p = self.proxify(v) - p.attribute = "aoeu" - v.attribute = "aoeu" - assert p.__subject__.attribute == v.attribute - del p.attribute - del v.attribute - assert not hasattr(v, "attribute") - assert not hasattr(p, "attribute") - - def test_call(self): - func = lambda a: a * 2 - p = self.proxify(func) - assert p(5) == func(5) - - def test_subject_attribute(self): - # Test getting subject - v = ["aoeu"] - p = LazyProxy(lambda: v) - assert p.__subject__ is v - # Test setting subject - v2 = "aoeu" - p.__subject__ = v2 - assert p == v2 - - def test_subclass_attributes(self): - class C(LazyProxy): - __notproxied__ = ("class_attr",) - class_attr = "aoeu" - - c = C(lambda: 3) - # Make sure proxy functionality still works - assert c == 3 - # Make sure subclass attr is accessible - assert c.class_attr == "aoeu" - # Make sure instance attribute is set on proxy - c.class_attr = "htns" - assert c.class_attr == "htns" - assert not hasattr(c.__subject__, "class_attr") - # Test instance attribute is deleted from proxy - del c.class_attr - assert c.class_attr == "aoeu" - - def test_no_proxy_during_subclass_methods(self): - class A(LazyProxy): - def setter(self, value): - self.attr = value - - class C(A): - __notproxied__ = ("getter", "setter", "__eq__") - - def __init__(self, value): - self.attr = 5 - super(C, self).__init__(lambda: value) - - def __equal__(self, other): - return False - - @property - def getter(self): - return self.attr - - def setter(self, value): - super(C, self).setter(value) - - @notproxied - def decorated(self): - return 2.0 - - @property - @notproxied - def decorated_prop(self): - return 3.0 - - C.getter2 = notproxied(lambda self: self.attr) - - c = C("proxied") - # Make sure super works - assert c == "proxied" - # The instance properties and methods should be able to read and write - # attributes to self without any proxying - assert c.getter == 5 - c.setter("aoeu") - assert c.getter == "aoeu" - # Even if they are added after the class is created - assert c.getter2() == "aoeu" - # The decorated methods and properties should automatically be added to - # the __notproxied__ list - assert "decorated" in C.__notproxied__ - assert c.decorated() == 2.0 - assert "decorated_prop" in C.__notproxied__ - assert c.decorated_prop == 3.0 - # Outside the methods it should still be proxied (str has no 'attr') - with pytest.raises(AttributeError): - c.attr = 1 - with pytest.raises(AttributeError): - c.attr diff --git a/tests/test_jsonref_errors.py b/tests/test_jsonref_errors.py new file mode 100644 index 0000000..0689656 --- /dev/null +++ b/tests/test_jsonref_errors.py @@ -0,0 +1,28 @@ +import pytest +from jsonref import replace_refs, JsonRefError + + +class TestJsonRefErrors(object): + def test_basic_error_properties(self): + json = [{"$ref": "#/x"}] + result = replace_refs(json) + with pytest.raises(JsonRefError) as excinfo: + result[0].__subject__ + e = excinfo.value + assert e.reference == json[0] + assert e.uri == "#/x" + assert e.base_uri == "" + assert e.path == (0,) + assert type(e.cause) == TypeError + + def test_nested_refs(self): + json = { + "a": {"$ref": "#/b"}, + "b": {"$ref": "#/c"}, + "c": {"$ref": "#/foo"}, + } + result = replace_refs(json) + with pytest.raises(JsonRefError) as excinfo: + print(result["a"]) + e = excinfo.value + assert e.path == ("c",) diff --git a/tests/test_proxies.py b/tests/test_proxies.py new file mode 100644 index 0000000..2cc4de5 --- /dev/null +++ b/tests/test_proxies.py @@ -0,0 +1,308 @@ +import functools +import operator +from copy import deepcopy + +import pytest + +from jsonref import replace_refs +from jsonref.proxytypes import CallbackProxy, LazyProxy, Proxy, notproxied # noqa: F401 + + +def cmp(a, b): + return (a > b) - (a < b) + + +@pytest.fixture( + params=[{"lazy_load": True}, {"lazy_load": False}, {"proxies": False}], + ids=["lazy_load", "no lazy_load", "no proxies"], +) +def parametrized_replace_refs(request): + return functools.partial(replace_refs, **request.param) + + +_unset = object() + + +class TestProxies(object): + @pytest.fixture( + scope="class", autouse=True, params=["Proxy", "CallbackProxy", "LazyProxy"] + ) + def make_proxify(self, request): + param = request.param + + def proxify(self, val): + c = deepcopy(val) + if param == "Proxy": + return Proxy(c) + globals().get(param) + return globals().get(param)(lambda: c) + + request.cls.proxify = proxify + + def check_func(self, func, value, other=_unset): + """ + Checks func works the same with `value` as when `value` is proxied. + + """ + + p = self.proxify(value) + args = [] + if other is not _unset: + args = [other] + try: + result = func(value, *args) + except Exception as e: + with pytest.raises(type(e)): + func(p, *args) + else: + assert func(p, *args) == result + # If this func takes two arguments, try them reversed as well + if other is not _unset: + try: + result = func(other, value) + except Exception as e: + with pytest.raises(type(e)): + func(other, p) + else: + assert func(other, p) == result + + def check_integer(self, v): + for op in ( + operator.and_, + operator.or_, + operator.xor, + operator.iand, + operator.ior, + operator.ixor, + ): + self.check_func(op, v, 0b10101) + for op in ( + operator.lshift, + operator.rshift, + operator.ilshift, + operator.irshift, + ): + self.check_func(op, v, 3) + for op in (operator.invert, hex, oct): + self.check_func(op, v) + + self.check_numeric(v) + + def check_numeric(self, v): + for op in (operator.pos, operator.neg, abs, int, float, hash, complex): + self.check_func(op, v) + + for other in (5, 13.7): # Check against both an int and a float + for op in ( + # Math + operator.mul, + operator.pow, + operator.add, + operator.sub, + operator.truediv, + operator.floordiv, + operator.mod, + divmod, + # In-place + operator.imul, + operator.ipow, + operator.iadd, + operator.isub, + operator.itruediv, + operator.ifloordiv, + operator.imod, + # Comparison + operator.lt, + operator.le, + operator.gt, + operator.ge, + operator.eq, + operator.ne, + cmp, + ): + self.check_func(op, v, other) + + self.check_basics(v) + + def check_list(self, v): + for i in range(len(v)): + for arg in (i, slice(i), slice(None, i), slice(i, None, -1)): + self.check_func(operator.getitem, v, arg) + self.check_container(v) + + p = self.proxify(v) + c = list(v) + + p[1:1] = [23] + c[1:1] = [23] + assert p == c + + p.insert(1, 0) + c.insert(1, 0) + assert p == c + + p += [4] + c += [4] + assert p == c + + del p[::2] + del c[::2] + assert p == c + + def check_container(self, v): + for op in (list, set, len, sorted, lambda x: list(iter(x))): + self.check_func(op, v) + self.check_basics(v) + + def check_basics(self, v): + for f in bool, repr, str: + self.check_func(f, v) + + def test_numbers(self): + for i in range(20): + self.check_integer(i) + + f = -40 + while f <= 20.0: + self.check_numeric(f) + f += 2.25 + + def test_lists(self): + for l in [1, 2], [3, 42, 59], [99, 23, 55], ["a", "b", 1.4, 17.3, -3, 42]: + self.check_list(l) + + def test_dicts(self): + for d in ({"a": 3, 4: 2, 1.5: "b"}, {}, {"": ""}): + for op in ( + sorted, + set, + len, + lambda x: sorted(iter(x)), + operator.methodcaller("get", "a"), + ): + self.check_func(op, d) + + p = self.proxify(d) + # Use sets to make sure order doesn't matter + assert set(p.items()) == set(d.items()) + assert set(p.keys()) == set(d.keys()) + assert set(p.values()) == set(d.values()) + + self.check_basics(d) + + def test_immutable(self): + a = self.proxify(3) + b = a + b += 3 + assert a == 3 + assert b == 6 + + def test_mutable(self): + a = self.proxify([0]) + b = a + b += [1] + assert a == [0, 1] + assert b == [0, 1] + + def test_attributes(self): + class C(object): + def __init__(self): + self.attribute = "value" + + v = C() + p = self.proxify(v) + p.attribute = "aoeu" + v.attribute = "aoeu" + assert p.__subject__.attribute == v.attribute + del p.attribute + del v.attribute + assert not hasattr(v, "attribute") + assert not hasattr(p, "attribute") + + def test_call(self): + func = lambda a: a * 2 + p = self.proxify(func) + assert p(5) == func(5) + + def test_subject_attribute(self): + # Test getting subject + v = ["aoeu"] + p = LazyProxy(lambda: v) + assert p.__subject__ is v + # Test setting subject + v2 = "aoeu" + p.__subject__ = v2 + assert p == v2 + + def test_subclass_attributes(self): + class C(LazyProxy): + __notproxied__ = ("class_attr",) + class_attr = "aoeu" + + c = C(lambda: 3) + # Make sure proxy functionality still works + assert c == 3 + # Make sure subclass attr is accessible + assert c.class_attr == "aoeu" + # Make sure instance attribute is set on proxy + c.class_attr = "htns" + assert c.class_attr == "htns" + assert not hasattr(c.__subject__, "class_attr") + # Test instance attribute is deleted from proxy + del c.class_attr + assert c.class_attr == "aoeu" + + def test_no_proxy_during_subclass_methods(self): + class A(LazyProxy): + def setter(self, value): + self.attr = value + + class C(A): + __notproxied__ = ("getter", "setter", "__eq__") + + def __init__(self, value): + self.attr = 5 + super(C, self).__init__(lambda: value) + + def __equal__(self, other): + return False + + @property + def getter(self): + return self.attr + + def setter(self, value): + super(C, self).setter(value) + + @notproxied + def decorated(self): + return 2.0 + + @property + @notproxied + def decorated_prop(self): + return 3.0 + + C.getter2 = notproxied(lambda self: self.attr) + + c = C("proxied") + # Make sure super works + assert c == "proxied" + # The instance properties and methods should be able to read and write + # attributes to self without any proxying + assert c.getter == 5 + c.setter("aoeu") + assert c.getter == "aoeu" + # Even if they are added after the class is created + assert c.getter2() == "aoeu" + # The decorated methods and properties should automatically be added to + # the __notproxied__ list + assert "decorated" in C.__notproxied__ + assert c.decorated() == 2.0 + assert "decorated_prop" in C.__notproxied__ + assert c.decorated_prop == 3.0 + # Outside the methods it should still be proxied (str has no 'attr') + with pytest.raises(AttributeError): + c.attr = 1 + with pytest.raises(AttributeError): + c.attr