diff --git a/.gitattributes b/.gitattributes index 7435e016..059035f1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ * text eol=lf shapefiles/**/* binary +tests/shapefiles/**/* binary diff --git a/.github/actions/test/action.yml b/.github/actions/test/action.yml index b15be0a4..e4a89d97 100644 --- a/.github/actions/test/action.yml +++ b/.github/actions/test/action.yml @@ -85,7 +85,7 @@ runs: working-directory: ${{ inputs.pyshp_repo_directory }} env: REPLACE_REMOTE_URLS_WITH_LOCALHOST: ${{ inputs.replace_remote_urls_with_localhost }} - run: python test_shapefile.py ${{ inputs.extra_args }} + run: python tests/run_doctests.py ${{ inputs.extra_args }} - name: Show Python and Pytest versions for logs. shell: bash diff --git a/.github/workflows/speed_test.yml b/.github/workflows/speed_test.yml index 39b45a13..a2a40bb7 100644 --- a/.github/workflows/speed_test.yml +++ b/.github/workflows/speed_test.yml @@ -65,7 +65,7 @@ jobs: - name: Run Speed tests. env: PYSHP_TEST_REPO: ./PyShp_test_shapefile - run: python ./Pyshp/run_benchmarks.py + run: python ./Pyshp/tests/run_benchmarks.py diff --git a/README.md b/README.md index 876ce37c..9b60f905 100644 --- a/README.md +++ b/README.md @@ -411,36 +411,36 @@ files. You specify the base filename of the shapefile or the complete filename of any of the shapefile component files. - >>> sf = shapefile.Reader("shapefiles/blockgroups") + >>> sf = shapefile.Reader("tests/shapefiles/blockgroups") OR - >>> sf = shapefile.Reader("shapefiles/blockgroups.shp") + >>> sf = shapefile.Reader("tests/shapefiles/blockgroups.shp") OR - >>> sf = shapefile.Reader("shapefiles/blockgroups.dbf") + >>> sf = shapefile.Reader("tests/shapefiles/blockgroups.dbf") OR any of the other 5+ formats which are potentially part of a shapefile. The library does not care about file extensions. You can also specify that you only want to read some of the file extensions through the use of keyword arguments: - >>> sf = shapefile.Reader(dbf="shapefiles/blockgroups.dbf") + >>> sf = shapefile.Reader(dbf="tests/shapefiles/blockgroups.dbf") #### Reading Shapefiles from Zip Files If your shapefile is wrapped inside a zip file, the library is able to handle that too, meaning you don't have to worry about unzipping the contents: - >>> sf = shapefile.Reader("shapefiles/blockgroups.zip") + >>> sf = shapefile.Reader("tests/shapefiles/blockgroups.zip") If the zip file contains multiple shapefiles, just specify which shapefile to read by additionally specifying the relative path after the ".zip" part: - >>> sf = shapefile.Reader("shapefiles/blockgroups_multishapefile.zip/blockgroups2.shp") + >>> sf = shapefile.Reader("tests/shapefiles/blockgroups_multishapefile.zip/blockgroups2.shp") #### Reading Shapefiles from URLs @@ -462,8 +462,8 @@ arguments to specify any of the three files. This feature is very powerful and allows you to custom load shapefiles from arbitrary storage formats, such as a protected url or zip file, a serialized object, or in some cases a database. - >>> myshp = open("shapefiles/blockgroups.shp", "rb") - >>> mydbf = open("shapefiles/blockgroups.dbf", "rb") + >>> myshp = open("tests/shapefiles/blockgroups.shp", "rb") + >>> mydbf = open("tests/shapefiles/blockgroups.dbf", "rb") >>> r = shapefile.Reader(shp=myshp, dbf=mydbf) Notice in the examples above the shx file is never used. The shx file is a @@ -477,7 +477,7 @@ it. The "Reader" class can be used as a context manager, to ensure open file objects are properly closed when done reading the data: - >>> with shapefile.Reader("shapefiles/blockgroups.shp") as shp: + >>> with shapefile.Reader("tests/shapefiles/blockgroups.shp") as shp: ... print(shp) shapefile Reader 663 shapes (type 'POLYGON') @@ -490,7 +490,7 @@ A shapefile is a container for a specific type of geometry, and this can be chec shapeType attribute. - >>> sf = shapefile.Reader("shapefiles/blockgroups.dbf") + >>> sf = shapefile.Reader("tests/shapefiles/blockgroups.dbf") >>> sf.shapeType 5 @@ -836,7 +836,7 @@ To create a shapefile you begin by initiating a new Writer instance, passing it the file path and name to save to: - >>> w = shapefile.Writer('shapefiles/test/testfile') + >>> w = shapefile.Writer('tests/shapefiles/test/testfile') >>> w.field('field1', 'C') File extensions are optional when reading or writing shapefiles. If you specify @@ -845,7 +845,7 @@ file name that is used for all three file types. Or you can specify a name for one or more file types: - >>> w = shapefile.Writer(dbf='shapefiles/test/onlydbf.dbf') + >>> w = shapefile.Writer(dbf='tests/shapefiles/test/onlydbf.dbf') >>> w.field('field1', 'C') In that case, any file types not assigned will not @@ -895,7 +895,7 @@ Alternatively, you can also use the "Writer" class as a context manager, to ensu objects are properly closed and final headers written once you exit the with-clause: - >>> with shapefile.Writer("shapefiles/test/contextwriter") as w: + >>> with shapefile.Writer("tests/shapefiles/test/contextwriter") as w: ... w.field('field1', 'C') ... pass @@ -912,7 +912,7 @@ There are three ways to set the shape type: To manually set the shape type for a Writer object when creating the Writer: - >>> w = shapefile.Writer('shapefiles/test/shapetype', shapeType=3) + >>> w = shapefile.Writer('tests/shapefiles/test/shapetype', shapeType=3) >>> w.field('field1', 'C') >>> w.shapeType @@ -938,7 +938,7 @@ Text fields are created using the 'C' type, and the third 'size' argument can be length of text values to save space: - >>> w = shapefile.Writer('shapefiles/test/dtype') + >>> w = shapefile.Writer('tests/shapefiles/test/dtype') >>> w.field('TEXT', 'C') >>> w.field('SHORT_TEXT', 'C', size=5) >>> w.field('LONG_TEXT', 'C', size=250) @@ -946,7 +946,7 @@ length of text values to save space: >>> w.record('Hello', 'World', 'World'*50) >>> w.close() - >>> r = shapefile.Reader('shapefiles/test/dtype') + >>> r = shapefile.Reader('tests/shapefiles/test/dtype') >>> assert r.record(0) == ['Hello', 'World', 'World'*50] >>> r.close() @@ -956,7 +956,7 @@ Field length or decimal have no impact on this type: >>> from datetime import date - >>> w = shapefile.Writer('shapefiles/test/dtype') + >>> w = shapefile.Writer('tests/shapefiles/test/dtype') >>> w.field('DATE', 'D') >>> w.null() >>> w.null() @@ -968,7 +968,7 @@ Field length or decimal have no impact on this type: >>> w.record(None) >>> w.close() - >>> r = shapefile.Reader('shapefiles/test/dtype') + >>> r = shapefile.Reader('tests/shapefiles/test/dtype') >>> assert r.record(0) == [date(1898,1,30)] >>> assert r.record(1) == [date(1998,1,30)] >>> assert r.record(2) == [date(1998,1,30)] @@ -982,7 +982,7 @@ To store very large numbers you must increase the field length size to the total (including comma and minus). - >>> w = shapefile.Writer('shapefiles/test/dtype') + >>> w = shapefile.Writer('tests/shapefiles/test/dtype') >>> w.field('INT', 'N') >>> w.field('LOWPREC', 'N', decimal=2) >>> w.field('MEDPREC', 'N', decimal=10) @@ -996,7 +996,7 @@ To store very large numbers you must increase the field length size to the total >>> w.record(None, None, None, None, None, None) >>> w.close() - >>> r = shapefile.Reader('shapefiles/test/dtype') + >>> r = shapefile.Reader('tests/shapefiles/test/dtype') >>> assert r.record(0) == [1, 1.32, 1.3217328, -3.2302e-25, 1.3217328, 10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000] >>> assert r.record(1) == [None, None, None, None, None, None] >>> r.close() @@ -1007,7 +1007,7 @@ This field can take True or False values, or 1 (True) or 0 (False). None is interpreted as missing. - >>> w = shapefile.Writer('shapefiles/test/dtype') + >>> w = shapefile.Writer('tests/shapefiles/test/dtype') >>> w.field('BOOLEAN', 'L') >>> w.null() >>> w.null() @@ -1023,7 +1023,7 @@ None is interpreted as missing. >>> w.record("Nonsense") >>> w.close() - >>> r = shapefile.Reader('shapefiles/test/dtype') + >>> r = shapefile.Reader('tests/shapefiles/test/dtype') >>> r.record(0) Record #0: [True] >>> r.record(1) @@ -1041,7 +1041,7 @@ None is interpreted as missing. You can also add attributes using keyword arguments where the keys are field names. - >>> w = shapefile.Writer('shapefiles/test/dtype') + >>> w = shapefile.Writer('tests/shapefiles/test/dtype') >>> w.field('FIRST_FLD','C', 40) >>> w.field('SECOND_FLD','C', 40) >>> w.null() @@ -1062,7 +1062,7 @@ A shapefile may contain some records for which geometry is not available, and ma Because Null shape types (shape type 0) have no geometry the "null" method is called without any arguments. - >>> w = shapefile.Writer('shapefiles/test/null') + >>> w = shapefile.Writer('tests/shapefiles/test/null') >>> w.field('name', 'C') >>> w.null() @@ -1076,7 +1076,7 @@ Point shapes are added using the "point" method. A point is specified by an x an y value. - >>> w = shapefile.Writer('shapefiles/test/point') + >>> w = shapefile.Writer('tests/shapefiles/test/point') >>> w.field('name', 'C') >>> w.point(122, 37) @@ -1090,7 +1090,7 @@ If your point data allows for the possibility of multiple points per feature, us These are specified as a list of xy point coordinates. - >>> w = shapefile.Writer('shapefiles/test/multipoint') + >>> w = shapefile.Writer('tests/shapefiles/test/multipoint') >>> w.field('name', 'C') >>> w.multipoint([[122,37], [124,32]]) @@ -1104,7 +1104,7 @@ For LineString shapefiles, each shape is given as a list of one or more linear f Each of the linear features must have at least two points. - >>> w = shapefile.Writer('shapefiles/test/line') + >>> w = shapefile.Writer('tests/shapefiles/test/line') >>> w.field('name', 'C') >>> w.line([ @@ -1128,7 +1128,7 @@ If any of the polygons have holes, then the hole polygon coordinates must be ord The direction of your polygons determines how shapefile readers will distinguish between polygon outlines and holes. - >>> w = shapefile.Writer('shapefiles/test/polygon') + >>> w = shapefile.Writer('tests/shapefiles/test/polygon') >>> w.field('name', 'C') >>> w.poly([ @@ -1147,9 +1147,9 @@ You can also pass it any GeoJSON dictionary or _\_geo_interface\_\_ compatible o This can be particularly useful for copying from one file to another: - >>> r = shapefile.Reader('shapefiles/test/polygon') + >>> r = shapefile.Reader('tests/shapefiles/test/polygon') - >>> w = shapefile.Writer('shapefiles/test/copy') + >>> w = shapefile.Writer('tests/shapefiles/test/copy') >>> w.fields = r.fields[1:] # skip first deletion field >>> # adding existing Shape objects @@ -1174,7 +1174,7 @@ must take care to add records and shapes in the same order so that the record data lines up with the geometry data. For example: - >>> w = shapefile.Writer('shapefiles/test/balancing', shapeType=shapefile.POINT) + >>> w = shapefile.Writer('tests/shapefiles/test/balancing', shapeType=shapefile.POINT) >>> w.field("field1", "C") >>> w.field("field2", "C") @@ -1269,7 +1269,7 @@ For reading shapefiles in any non-utf8 encoding, such as Latin-1, just supply the encoding option when creating the Reader class. - >>> r = shapefile.Reader("shapefiles/latin1.shp", encoding="latin1") + >>> r = shapefile.Reader("tests/shapefiles/latin1.shp", encoding="latin1") >>> r.record(0) == [2, u'Ñandú'] True @@ -1278,14 +1278,14 @@ as UTF-8. Assuming the new encoding supports the characters you are trying to wr should give you the same unicode string you started with. - >>> w = shapefile.Writer("shapefiles/test/latin_as_utf8.shp", encoding="utf8") + >>> w = shapefile.Writer("tests/shapefiles/test/latin_as_utf8.shp", encoding="utf8") >>> w.fields = r.fields[1:] >>> w.record(*r.record(0)) >>> w.null() >>> r.close() >>> w.close() - >>> r = shapefile.Reader("shapefiles/test/latin_as_utf8.shp", encoding="utf8") + >>> r = shapefile.Reader("tests/shapefiles/test/latin_as_utf8.shp", encoding="utf8") >>> r.record(0) == [2, u'Ñandú'] True >>> r.close() @@ -1296,7 +1296,7 @@ or replace encoding errors, you can specify the "encodingErrors" to be used by t applies to both reading and writing. - >>> r = shapefile.Reader("shapefiles/latin1.shp", encoding="ascii", encodingErrors="replace") + >>> r = shapefile.Reader("tests/shapefiles/latin1.shp", encoding="ascii", encodingErrors="replace") >>> r.record(0) == [2, u'�and�'] True >>> r.close() @@ -1422,17 +1422,17 @@ long as you iterate through the source files to avoid loading everything into memory. The following example copies the contents of a shapefile to a new file 10 times: >>> # create writer - >>> w = shapefile.Writer('shapefiles/test/merge') + >>> w = shapefile.Writer('tests/shapefiles/test/merge') >>> # copy over fields from the reader - >>> r = shapefile.Reader("shapefiles/blockgroups") + >>> r = shapefile.Reader("tests/shapefiles/blockgroups") >>> for field in r.fields[1:]: ... w.field(*field) >>> # copy the shapefile to writer 10 times >>> repeat = 10 >>> for i in range(repeat): - ... r = shapefile.Reader("shapefiles/blockgroups") + ... r = shapefile.Reader("tests/shapefiles/blockgroups") ... for shapeRec in r.iterShapeRecords(): ... w.record(*shapeRec.record) ... w.shape(shapeRec.shape) @@ -1452,13 +1452,13 @@ If you need to edit a shapefile you would have to read the file one record at a time, modify or filter the contents, and write it back out. For instance, to create a copy of a shapefile that only keeps a subset of relevant fields: >>> # create writer - >>> w = shapefile.Writer('shapefiles/test/edit') + >>> w = shapefile.Writer('tests/shapefiles/test/edit') >>> # define which fields to keep >>> keep_fields = ['BKG_KEY', 'MEDIANRENT'] >>> # copy over the relevant fields from the reader - >>> r = shapefile.Reader("shapefiles/blockgroups") + >>> r = shapefile.Reader("tests/shapefiles/blockgroups") >>> for field in r.fields[1:]: ... if field[0] in keep_fields: ... w.field(*field) @@ -1485,7 +1485,7 @@ third M value to each XY coordinate. Missing or unobserved M-values are specifie or by simply omitting the third M-coordinate. - >>> w = shapefile.Writer('shapefiles/test/linem') + >>> w = shapefile.Writer('tests/shapefiles/test/linem') >>> w.field('name', 'C') >>> w.linem([ @@ -1499,7 +1499,7 @@ or by simply omitting the third M-coordinate. Shapefiles containing M-values can be examined in several ways: - >>> r = shapefile.Reader('shapefiles/test/linem') + >>> r = shapefile.Reader('tests/shapefiles/test/linem') >>> r.mbox # the lower and upper bound of M-values in the shapefile MBox(mmin=0.0, mmax=3.0) @@ -1519,7 +1519,7 @@ but if you omit the third Z-coordinate it will default to 0. Note that Z-type sh as a fourth M-coordinate. This too is optional. - >>> w = shapefile.Writer('shapefiles/test/linez') + >>> w = shapefile.Writer('tests/shapefiles/test/linez') >>> w.field('name', 'C') >>> w.linez([ @@ -1534,7 +1534,7 @@ as a fourth M-coordinate. This too is optional. To examine a Z-type shapefile you can do: - >>> r = shapefile.Reader('shapefiles/test/linez') + >>> r = shapefile.Reader('tests/shapefiles/test/linez') >>> r.zbox # the lower and upper bound of Z-values in the shapefile ZBox(zmin=0.0, zmax=22.0) @@ -1556,7 +1556,7 @@ its roof: >>> from shapefile import TRIANGLE_STRIP, TRIANGLE_FAN - >>> w = shapefile.Writer('shapefiles/test/multipatch') + >>> w = shapefile.Writer('tests/shapefiles/test/multipatch') >>> w.field('name', 'C') >>> w.multipatch([ @@ -1576,10 +1576,10 @@ ESRI White Paper](http://downloads.esri.com/support/whitepapers/ao_/J9749_MultiP # Testing -The testing framework is pytest, and the tests are located in test_shapefile.py. -This includes an extensive set of unit tests of the various pyshp features, +The testing framework is pytest, and the tests are located in ./tests. +This includes an extensive set of unit tests of the various pyshp features in ./tests/test_shapefile.py, and tests against various input data. -In the same folder as README.md and shapefile.py, from the command line run: +In the same folder as README.md and pyproject.toml, from the command line run: ```shell python -m pytest @@ -1587,20 +1587,11 @@ python -m pytest Additionally, all the code and examples located in this file, README.md, is tested and verified with the builtin doctest framework. -A special routine for invoking the doctest is run when calling directly on shapefile.py. -In the same folder as README.md and shapefile.py, from the command line run: +A special doctest runner will run the doctests, with test artefacts confined to a +a temporary directory, and optionally switching the remote URLs for localhost: ```shell -python shapefile.py -``` - -This tests the code inside shapefile.py itself. To test an installed PyShp wheel against -the doctests, the same special routine can be invoked (in an env with the wheel and pytest -installed) from the test file: - - -```shell -python test_shapefile.py +python tests/run_doctests.py ``` @@ -1616,7 +1607,7 @@ pytest -m "not network" ``` or the doctests via: ```shell -python test_shapefile.py -m "not network" +python tests/run_doctests.py -m "not network" ``` or ii) by cloning a repo of the files they download, serving these on localhost in a separate process, and running the network tests with the environment variable REPLACE_REMOTE_URLS_WITH_LOCALHOST to `yes`: @@ -1632,10 +1623,10 @@ REPLACE_REMOTE_URLS_WITH_LOCALHOST=yes && pytest ``` or the doctests via: ```bash -REPLACE_REMOTE_URLS_WITH_LOCALHOST=yes && python test_shapefile.py +REPLACE_REMOTE_URLS_WITH_LOCALHOST=yes && python tests/run_doctests.py ``` The network tests alone can also be run (without also running all the tests that don't -make network requests) using: `pytest -m network` (or the doctests using: `python shapefile.py -m network`). +make network requests) using: `pytest -m network` (or the doctests using: `python tests/run_doctests.py -m network`). diff --git a/pyproject.toml b/pyproject.toml index 1413cc3c..11f7c53e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,13 +54,14 @@ mypy_path = "src" explicit_package_bases = true exclude_gitignore = true exclude=[ # Mypy requires regexes, not globs: - 'test_shapefile\.py', - 'run_benchmarks\.py', + 'tests/test_shapefile\.py', + 'tests/run_benchmarks\.py', + 'tests/run_doctests\.py', ] [tool.hatch.build.targets.sdist] -only-include = ["src", "shapefiles", "test_shapefile.py"] +only-include = ["src", "tests"] [tool.hatch.build.targets.wheel] only-include = ["src"] diff --git a/src/shapefile.py b/src/shapefile.py index 8b9454af..63a675da 100644 --- a/src/shapefile.py +++ b/src/shapefile.py @@ -12,13 +12,11 @@ import abc import array -import doctest import functools import io import itertools import logging import os -import re import sys import tempfile import time @@ -58,14 +56,10 @@ # Create named logger logger = logging.getLogger(__name__) + # Module settings VERBOSE = True -# Test config (for the Doctest runner and test_shapefile.py) -REPLACE_REMOTE_URLS_WITH_LOCALHOST = ( - os.getenv("REPLACE_REMOTE_URLS_WITH_LOCALHOST", "").lower() == "true" -) - IS_WINDOWS = sys.platform == "win32" # Constants for shape types @@ -4618,165 +4612,3 @@ def multipatch(self, parts: list[PointsT], partTypes: list[int]) -> None: If the m (measure) value is not included, it defaults to None (NoData).""" shape = MultiPatch(lines=parts, partTypes=partTypes) self.shape(shape) - - -# Begin Testing -def _get_doctests() -> doctest.DocTest: - # run tests - with open("README.md", "rb") as fobj: - tests = doctest.DocTestParser().get_doctest( - string=fobj.read().decode("utf8").replace("\r\n", "\n"), - globs={}, - name="README", - filename="README.md", - lineno=0, - ) - - return tests - - -def _filter_network_doctests( - examples: Iterable[doctest.Example], - include_network: bool = False, - include_non_network: bool = True, -) -> Iterator[doctest.Example]: - globals_from_network_doctests = set() - - if not (include_network or include_non_network): - return - - examples_it = iter(examples) - - yield next(examples_it) - - for example in examples_it: - # Track variables in doctest shell sessions defined from commands - # that poll remote URLs, to skip subsequent commands until all - # such dependent variables are reassigned. - - if 'sf = shapefile.Reader("https://' in example.source: - globals_from_network_doctests.add("sf") - if include_network: - yield example - continue - - lhs = example.source.partition("=")[0] - - for target in lhs.split(","): - target = target.strip() - if target in globals_from_network_doctests: - globals_from_network_doctests.remove(target) - - # Non-network tests dependent on the network tests. - if globals_from_network_doctests: - if include_network: - yield example - continue - - if not include_non_network: - continue - - yield example - - -def _replace_remote_url_with_localhost(old_url: str) -> str: - - old_split = urlsplit(old_url) - - # Strip subpaths, so an artefacts - # repo or file tree can be simpler and flat - path = old_split.path.rpartition("/")[2] - - new_split = old_split._replace( - scheme="http", - netloc="localhost:8000", # Default port of Python http.server - path=path, - query="", - fragment="", - ) - - return str(urlunsplit(new_split)) - - -_URL_STR_LITERAL_PATTERN = r'"(https?://.*)"' - - -def _change_remote_url_match_to_localhost( - match: re.Match[Any], # A Match from _URL_STR_LITERAL_PATTERN above -) -> str: - - old_url = match.group(1) - new_url = _replace_remote_url_with_localhost(old_url) - return f'"{new_url}"' - - -def _test( - temp_dir: str | None = None, - args: list[str] = sys.argv[1:], - verbosity: bool = False, -) -> int: - - if verbosity == 0: - print("Getting doctests...") - - tests = _get_doctests() - - if len(args) >= 2 and args[0] == "-m": - if verbosity == 0: - print("Filtering doctests...") - tests.examples = list( - _filter_network_doctests( - tests.examples, - include_network=args[1] == "network", - include_non_network=args[1] == "not network", - ) - ) - - if REPLACE_REMOTE_URLS_WITH_LOCALHOST: - if verbosity == 0: - print("Replacing remote urls with http://localhost in doctests...") - - for example in tests.examples: - example.source = re.sub( - pattern=_URL_STR_LITERAL_PATTERN, - repl=_change_remote_url_match_to_localhost, - string=example.source, - ) - - if temp_dir is not None: - for example in tests.examples: - example.source = example.source.replace("shapefiles/test/", f"{temp_dir}/") - - runner = doctest.DocTestRunner(verbose=verbosity, optionflags=doctest.FAIL_FAST) - - if verbosity == 0: - print(f"Running {len(tests.examples)} doctests...") - # Deleting a temp dir will error if it contains shapefiles to which - # unclosed Readers still file objects open, - # regardless of using clear_globs=True or calling gc.collect afterwards. - failure_count, __test_count = runner.run(tests) - - # print results - if verbosity: - runner.summarize(True) - else: - if failure_count == 0: - print("All test passed successfully") - elif failure_count > 0: - runner.summarize(verbosity) - - return failure_count - - -def main() -> None: - """ - Doctests are contained in the file 'README.md', and are tested using the built-in - testing libraries. - """ - with tempfile.TemporaryDirectory() as td: - failure_count = _test(Path(td).as_posix()) - sys.exit(failure_count) - - -if __name__ == "__main__": - main() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..28be1461 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +"""Make tests a package so that test_shapefile.py can +import some helpers from run_doctests.py. +""" diff --git a/run_benchmarks.py b/tests/run_benchmarks.py similarity index 100% rename from run_benchmarks.py rename to tests/run_benchmarks.py diff --git a/tests/run_doctests.py b/tests/run_doctests.py new file mode 100644 index 00000000..03930d46 --- /dev/null +++ b/tests/run_doctests.py @@ -0,0 +1,210 @@ +from __future__ import annotations + +import argparse +import doctest +import os +import re +import sys +import tempfile +from collections.abc import Iterable, Iterator +from pathlib import Path +from typing import Any +from urllib.parse import urlsplit, urlunsplit + +DEFAULT_README = Path(__file__).parent.parent / "README.md" + + +# Test config (for the Doctest runner and test_shapefile.py) +REPLACE_REMOTE_URLS_WITH_LOCALHOST = ( + os.getenv("REPLACE_REMOTE_URLS_WITH_LOCALHOST", "").lower() == "true" +) + + +# Begin Testing +def _get_doctests(readme: Path = DEFAULT_README) -> doctest.DocTest: + # run tests + with readme.open("rb") as fobj: + tests = doctest.DocTestParser().get_doctest( + string=fobj.read().decode("utf8").replace("\r\n", "\n"), + globs={}, + name=fobj.name, # readme.stem, + filename=fobj.name, # readme.name, + lineno=0, + ) + + return tests + + +def _filter_network_doctests( + examples: Iterable[doctest.Example], + include_network: bool = False, + include_non_network: bool = True, +) -> Iterator[doctest.Example]: + globals_from_network_doctests = set() + + if not (include_network or include_non_network): + return + + examples_it = iter(examples) + + yield next(examples_it) + + for example in examples_it: + # Track variables in doctest shell sessions defined from commands + # that poll remote URLs, to skip subsequent commands until all + # such dependent variables are reassigned. + + if 'sf = shapefile.Reader("https://' in example.source: + globals_from_network_doctests.add("sf") + if include_network: + yield example + continue + + lhs = example.source.partition("=")[0] + + for target in lhs.split(","): + target = target.strip() + if target in globals_from_network_doctests: + globals_from_network_doctests.remove(target) + + # Non-network tests dependent on the network tests. + if globals_from_network_doctests: + if include_network: + yield example + continue + + if not include_non_network: + continue + + yield example + + +def _replace_remote_url_with_localhost(old_url: str) -> str: + + old_split = urlsplit(old_url) + + # Strip subpaths, so an artefacts + # repo or file tree can be simpler and flat + path = old_split.path.rpartition("/")[2] + + new_split = old_split._replace( + scheme="http", + netloc="localhost:8000", # Default port of Python http.server + path=path, + query="", + fragment="", + ) + + return str(urlunsplit(new_split)) + + +_URL_STR_LITERAL_PATTERN = r'"(https?://.*)"' + + +def _change_remote_url_match_to_localhost( + match: re.Match[Any], # A Match from _URL_STR_LITERAL_PATTERN above +) -> str: + + old_url = match.group(1) + new_url = _replace_remote_url_with_localhost(old_url) + return f'"{new_url}"' + + +def _test( + temp_dir: str | None = None, + readme: Path = DEFAULT_README, + include_network: bool = False, + include_non_network: bool = True, + verbosity: bool = False, +) -> int: + + if verbosity == 0: + print("Getting doctests...") + + tests = _get_doctests(readme=readme) + + if verbosity == 0: + print("Filtering doctests...") + tests.examples = list( + _filter_network_doctests( + tests.examples, + include_network=include_network, + include_non_network=include_non_network, + ) + ) + + if REPLACE_REMOTE_URLS_WITH_LOCALHOST: + if verbosity == 0: + print("Replacing remote urls with http://localhost in doctests...") + + for example in tests.examples: + example.source = re.sub( + pattern=_URL_STR_LITERAL_PATTERN, + repl=_change_remote_url_match_to_localhost, + string=example.source, + ) + + if temp_dir is not None: + for example in tests.examples: + example.source = example.source.replace( + "tests/shapefiles/test/", f"{temp_dir}/" + ) + + runner = doctest.DocTestRunner(verbose=verbosity, optionflags=doctest.FAIL_FAST) + + if verbosity == 0: + print(f"Running {len(tests.examples)} doctests...") + # Deleting a temp dir will error if it contains shapefiles to which + # unclosed Readers still file objects open, + # regardless of using clear_globs=True or calling gc.collect afterwards. + failure_count, __test_count = runner.run(tests) + + # print results + if verbosity: + runner.summarize(True) + else: + if failure_count == 0: + print("All test passed successfully") + elif failure_count > 0: + runner.summarize(verbosity) + + return failure_count + + +def main() -> None: + """ + Doctests are contained in the file 'README.md', and are tested using the built-in + testing libraries. + """ + + parser = argparse.ArgumentParser( + prog="run_doctests.py", + description="PyShp's doctest runner", + ) + + parser.add_argument( + "--readme", + type=Path, + default=DEFAULT_README, + ) + parser.add_argument( + "-m", + type=str, + default="not network", + ) + namespace = parser.parse_args() + + with tempfile.TemporaryDirectory() as td: + failure_count = _test( + temp_dir=Path(td).as_posix(), + readme=namespace.readme, + include_network=namespace.m.lower().strip() == "network", + include_non_network=namespace.m.lower().strip() == "not network", + ) + sys.exit(failure_count) + + +# This allows a PyShp wheel installed in the env to be tested +# against the doctests. +if __name__ == "__main__": + main() diff --git a/shapefiles/ATTRIBUTION b/tests/shapefiles/ATTRIBUTION similarity index 100% rename from shapefiles/ATTRIBUTION rename to tests/shapefiles/ATTRIBUTION diff --git a/shapefiles/REL.zip b/tests/shapefiles/REL.zip similarity index 100% rename from shapefiles/REL.zip rename to tests/shapefiles/REL.zip diff --git a/shapefiles/blockgroups.dbf b/tests/shapefiles/blockgroups.dbf similarity index 100% rename from shapefiles/blockgroups.dbf rename to tests/shapefiles/blockgroups.dbf diff --git a/shapefiles/blockgroups.sbn b/tests/shapefiles/blockgroups.sbn similarity index 100% rename from shapefiles/blockgroups.sbn rename to tests/shapefiles/blockgroups.sbn diff --git a/shapefiles/blockgroups.sbx b/tests/shapefiles/blockgroups.sbx similarity index 100% rename from shapefiles/blockgroups.sbx rename to tests/shapefiles/blockgroups.sbx diff --git a/shapefiles/blockgroups.shp b/tests/shapefiles/blockgroups.shp similarity index 100% rename from shapefiles/blockgroups.shp rename to tests/shapefiles/blockgroups.shp diff --git a/shapefiles/blockgroups.shx b/tests/shapefiles/blockgroups.shx similarity index 100% rename from shapefiles/blockgroups.shx rename to tests/shapefiles/blockgroups.shx diff --git a/shapefiles/blockgroups.zip b/tests/shapefiles/blockgroups.zip similarity index 100% rename from shapefiles/blockgroups.zip rename to tests/shapefiles/blockgroups.zip diff --git a/shapefiles/blockgroups_multishapefile.zip b/tests/shapefiles/blockgroups_multishapefile.zip similarity index 100% rename from shapefiles/blockgroups_multishapefile.zip rename to tests/shapefiles/blockgroups_multishapefile.zip diff --git a/shapefiles/corrupt_too_long.dbf b/tests/shapefiles/corrupt_too_long.dbf similarity index 100% rename from shapefiles/corrupt_too_long.dbf rename to tests/shapefiles/corrupt_too_long.dbf diff --git a/shapefiles/corrupt_too_long.shp b/tests/shapefiles/corrupt_too_long.shp similarity index 100% rename from shapefiles/corrupt_too_long.shp rename to tests/shapefiles/corrupt_too_long.shp diff --git a/shapefiles/corrupt_too_long.shx b/tests/shapefiles/corrupt_too_long.shx similarity index 100% rename from shapefiles/corrupt_too_long.shx rename to tests/shapefiles/corrupt_too_long.shx diff --git a/shapefiles/edit.dbf b/tests/shapefiles/edit.dbf similarity index 100% rename from shapefiles/edit.dbf rename to tests/shapefiles/edit.dbf diff --git a/shapefiles/edit.shp b/tests/shapefiles/edit.shp similarity index 100% rename from shapefiles/edit.shp rename to tests/shapefiles/edit.shp diff --git a/shapefiles/edit.shx b/tests/shapefiles/edit.shx similarity index 100% rename from shapefiles/edit.shx rename to tests/shapefiles/edit.shx diff --git a/shapefiles/empty_zipfile.zip b/tests/shapefiles/empty_zipfile.zip similarity index 100% rename from shapefiles/empty_zipfile.zip rename to tests/shapefiles/empty_zipfile.zip diff --git a/shapefiles/latin1.dbf b/tests/shapefiles/latin1.dbf similarity index 100% rename from shapefiles/latin1.dbf rename to tests/shapefiles/latin1.dbf diff --git a/shapefiles/latin1.shp b/tests/shapefiles/latin1.shp similarity index 100% rename from shapefiles/latin1.shp rename to tests/shapefiles/latin1.shp diff --git a/shapefiles/merge.dbf b/tests/shapefiles/merge.dbf similarity index 100% rename from shapefiles/merge.dbf rename to tests/shapefiles/merge.dbf diff --git a/shapefiles/merge.shp b/tests/shapefiles/merge.shp similarity index 100% rename from shapefiles/merge.shp rename to tests/shapefiles/merge.shp diff --git a/shapefiles/merge.shx b/tests/shapefiles/merge.shx similarity index 100% rename from shapefiles/merge.shx rename to tests/shapefiles/merge.shx diff --git a/test_shapefile.py b/tests/test_shapefile.py similarity index 91% rename from test_shapefile.py rename to tests/test_shapefile.py index d90843e2..9a6baffa 100644 --- a/test_shapefile.py +++ b/tests/test_shapefile.py @@ -10,9 +10,15 @@ # third party imports import pytest -# our imports +# our imports (the code under test) import shapefile +# helper functions +from .run_doctests import ( + REPLACE_REMOTE_URLS_WITH_LOCALHOST, + _replace_remote_url_with_localhost, +) + shapefiles_dir = Path(__file__).parent / "shapefiles" # define various test shape tuples of (type, points, parts indexes, and expected geo interface output) @@ -452,7 +458,7 @@ def test_expected_shape_geo_interface(typ, points, parts, expected): def test_reader_geo_interface(): - with shapefile.Reader("shapefiles/blockgroups") as r: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups") as r: geoj = r.__geo_interface__ assert geoj["type"] == "FeatureCollection" assert "bbox" in geoj @@ -460,27 +466,27 @@ def test_reader_geo_interface(): def test_shapes_geo_interface(): - with shapefile.Reader("shapefiles/blockgroups") as r: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups") as r: geoj = r.shapes().__geo_interface__ assert geoj["type"] == "GeometryCollection" assert json.dumps(geoj) def test_shaperecords_geo_interface(): - with shapefile.Reader("shapefiles/blockgroups") as r: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups") as r: geoj = r.shapeRecords().__geo_interface__ assert geoj["type"] == "FeatureCollection" assert json.dumps(geoj) def test_shaperecord_geo_interface(): - with shapefile.Reader("shapefiles/blockgroups") as r: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups") as r: for shaperec in r: assert json.dumps(shaperec.__geo_interface__) @pytest.mark.skipif( - not shapefile.REPLACE_REMOTE_URLS_WITH_LOCALHOST, + not REPLACE_REMOTE_URLS_WITH_LOCALHOST, reason="Flakey test, fails due to Github rate limit", ) @pytest.mark.network @@ -495,7 +501,7 @@ def test_reader_nvkelso_files_from_localhost_url(): # defined in ./.github/actions/test/actions.yml def Reader(url): - new_url = shapefile._replace_remote_url_with_localhost(url) + new_url = _replace_remote_url_with_localhost(url) print(f"repr(new_url): {repr(new_url)}") return shapefile.Reader(new_url) @@ -529,10 +535,10 @@ def test_reader_urls(): # overloading external servers, and associated spurious test failures). # A suitable repo of test files, and a localhost server setup is # defined in ./.github/actions/test/actions.yml - if shapefile.REPLACE_REMOTE_URLS_WITH_LOCALHOST: + if REPLACE_REMOTE_URLS_WITH_LOCALHOST: def Reader(url): - new_url = shapefile._replace_remote_url_with_localhost(url) + new_url = _replace_remote_url_with_localhost(url) print(f"repr(new_url): {repr(new_url)}") return shapefile.Reader(new_url) else: @@ -576,7 +582,7 @@ def test_reader_zip(): Assert that Reader can open shapefiles inside a zipfile. """ # test reading zipfile only - with shapefile.Reader("shapefiles/blockgroups.zip") as sf: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups.zip") as sf: for __recShape in sf.iterShapeRecords(): pass assert len(sf) > 0 @@ -584,12 +590,14 @@ def test_reader_zip(): # test require specific path when reading multi-shapefile zipfile with pytest.raises(shapefile.ShapefileException): - with shapefile.Reader("shapefiles/blockgroups_multishapefile.zip") as sf: + with shapefile.Reader( + f"{shapefiles_dir.as_posix()}/blockgroups_multishapefile.zip" + ) as sf: pass # test specifying the path when reading multi-shapefile zipfile (with extension) with shapefile.Reader( - "shapefiles/blockgroups_multishapefile.zip/blockgroups2.shp" + f"{shapefiles_dir.as_posix()}/blockgroups_multishapefile.zip/blockgroups2.shp" ) as sf: for __recShape in sf.iterShapeRecords(): pass @@ -598,7 +606,7 @@ def test_reader_zip(): # test specifying the path when reading multi-shapefile zipfile (without extension) with shapefile.Reader( - "shapefiles/blockgroups_multishapefile.zip/blockgroups2" + f"{shapefiles_dir.as_posix()}/blockgroups_multishapefile.zip/blockgroups2" ) as sf: for __recShape in sf.iterShapeRecords(): pass @@ -607,7 +615,7 @@ def test_reader_zip(): # test raising error when can't find shapefile inside zipfile with pytest.raises(shapefile.ShapefileException): - with shapefile.Reader("shapefiles/empty_zipfile.zip") as sf: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/empty_zipfile.zip") as sf: pass @@ -619,7 +627,7 @@ def test_reader_close_path(): """ # note uses an actual shapefile from # the projects "shapefiles" directory - sf = shapefile.Reader("shapefiles/blockgroups.shp") + sf = shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups.shp") sf.close() assert sf.shp.closed is True @@ -627,7 +635,7 @@ def test_reader_close_path(): assert sf.shx.closed is True # check that can read again - sf = shapefile.Reader("shapefiles/blockgroups.shp") + sf = shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups.shp") sf.close() @@ -639,9 +647,9 @@ def test_reader_close_filelike(): """ # note uses an actual shapefile from # the projects "shapefiles" directory - shp = open("shapefiles/blockgroups.shp", mode="rb") - shx = open("shapefiles/blockgroups.shx", mode="rb") - dbf = open("shapefiles/blockgroups.dbf", mode="rb") + shp = open(f"{shapefiles_dir.as_posix()}/blockgroups.shp", mode="rb") + shx = open(f"{shapefiles_dir.as_posix()}/blockgroups.shx", mode="rb") + dbf = open(f"{shapefiles_dir.as_posix()}/blockgroups.dbf", mode="rb") sf = shapefile.Reader(shp=shp, shx=shx, dbf=dbf) sf.close() @@ -662,7 +670,7 @@ def test_reader_context_path(): """ # note uses an actual shapefile from # the projects "shapefiles" directory - with shapefile.Reader("shapefiles/blockgroups") as sf: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups") as sf: pass assert sf.shp.closed is True @@ -670,7 +678,7 @@ def test_reader_context_path(): assert sf.shx.closed is True # check that can read again - with shapefile.Reader("shapefiles/blockgroups") as sf: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups") as sf: pass @@ -682,9 +690,9 @@ def test_reader_context_filelike(): """ # note uses an actual shapefile from # the projects "shapefiles" directory - shp = open("shapefiles/blockgroups.shp", mode="rb") - shx = open("shapefiles/blockgroups.shx", mode="rb") - dbf = open("shapefiles/blockgroups.dbf", mode="rb") + shp = open(f"{shapefiles_dir.as_posix()}/blockgroups.shp", mode="rb") + shx = open(f"{shapefiles_dir.as_posix()}/blockgroups.shx", mode="rb") + dbf = open(f"{shapefiles_dir.as_posix()}/blockgroups.dbf", mode="rb") with shapefile.Reader(shp=shp, shx=shx, dbf=dbf) as sf: pass @@ -702,7 +710,7 @@ def test_reader_shapefile_type(): Assert that the type of the shapefile is returned correctly. """ - with shapefile.Reader("shapefiles/blockgroups") as sf: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups") as sf: assert sf.shapeType == 5 # 5 means Polygon assert sf.shapeType == shapefile.POLYGON assert sf.shapeTypeName == "POLYGON" @@ -714,12 +722,12 @@ def test_reader_shapefile_length(): matches up with the number of records in the file. """ - with shapefile.Reader("shapefiles/blockgroups") as sf: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups") as sf: assert len(sf) == len(sf.shapes()) def test_shape_metadata(): - with shapefile.Reader("shapefiles/blockgroups") as sf: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups") as sf: shape = sf.shape(0) assert shape.shapeType == 5 # Polygon assert shape.shapeType == shapefile.POLYGON @@ -733,7 +741,7 @@ def test_reader_fields(): Assert that each field has a name, type, field length, and decimal length. """ - with shapefile.Reader("shapefiles/blockgroups") as sf: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups") as sf: fields = sf.fields assert isinstance(fields, list) @@ -749,7 +757,7 @@ def test_reader_shapefile_extension_ignored(): Assert that the filename's extension is ignored when reading a shapefile. """ - base = "shapefiles/blockgroups" + base = f"{shapefiles_dir.as_posix()}/blockgroups" ext = ".abc" filename = base + ext with shapefile.Reader(filename) as sf: @@ -773,7 +781,7 @@ def test_reader_dbf_only(): dbf argument to the shapefile reader reads just the dbf file. """ - with shapefile.Reader(dbf="shapefiles/blockgroups.dbf") as sf: + with shapefile.Reader(dbf=f"{shapefiles_dir.as_posix()}/blockgroups.dbf") as sf: assert len(sf) == 663 record = sf.record(3) assert record[1:3] == ["060750601001", 4715] @@ -798,7 +806,8 @@ def test_reader_shp_shx_only(): reads just the shp and shx file. """ with shapefile.Reader( - shp="shapefiles/blockgroups.shp", shx="shapefiles/blockgroups.shx" + shp=f"{shapefiles_dir.as_posix()}/blockgroups.shp", + shx=f"{shapefiles_dir.as_posix()}/blockgroups.shx", ) as sf: assert len(sf) == 663 shape = sf.shape(3) @@ -826,7 +835,8 @@ def test_reader_shp_dbf_only(): reads just the shp and dbf file. """ with shapefile.Reader( - shp="shapefiles/blockgroups.shp", dbf="shapefiles/blockgroups.dbf" + shp=f"{shapefiles_dir.as_posix()}/blockgroups.shp", + dbf=f"{shapefiles_dir.as_posix()}/blockgroups.dbf", ) as sf: assert len(sf) == 663 shape = sf.shape(3) @@ -857,7 +867,7 @@ def test_reader_shp_only(): shp argument to the shapefile reader reads just the shp file (shx optional). """ - with shapefile.Reader(shp="shapefiles/blockgroups.shp") as sf: + with shapefile.Reader(shp=f"{shapefiles_dir.as_posix()}/blockgroups.shp") as sf: assert len(sf) == 663 shape = sf.shape(3) assert len(shape.points) == 173 @@ -881,7 +891,9 @@ def test_reader_filelike_dbf_only(): dbf argument to the shapefile reader reads just the dbf file. """ - with shapefile.Reader(dbf=open("shapefiles/blockgroups.dbf", "rb")) as sf: + with shapefile.Reader( + dbf=open(f"{shapefiles_dir.as_posix()}/blockgroups.dbf", "rb") + ) as sf: assert len(sf) == 663 record = sf.record(3) assert record[1:3] == ["060750601001", 4715] @@ -894,8 +906,8 @@ def test_reader_filelike_shp_shx_only(): reads just the shp and shx file. """ with shapefile.Reader( - shp=open("shapefiles/blockgroups.shp", "rb"), - shx=open("shapefiles/blockgroups.shx", "rb"), + shp=open(f"{shapefiles_dir.as_posix()}/blockgroups.shp", "rb"), + shx=open(f"{shapefiles_dir.as_posix()}/blockgroups.shx", "rb"), ) as sf: assert len(sf) == 663 shape = sf.shape(3) @@ -909,8 +921,8 @@ def test_reader_filelike_shp_dbf_only(): reads just the shp and dbf file. """ with shapefile.Reader( - shp=open("shapefiles/blockgroups.shp", "rb"), - dbf=open("shapefiles/blockgroups.dbf", "rb"), + shp=open(f"{shapefiles_dir.as_posix()}/blockgroups.shp", "rb"), + dbf=open(f"{shapefiles_dir.as_posix()}/blockgroups.dbf", "rb"), ) as sf: assert len(sf) == 663 shape = sf.shape(3) @@ -925,7 +937,9 @@ def test_reader_filelike_shp_only(): shp argument to the shapefile reader reads just the shp file (shx optional). """ - with shapefile.Reader(shp=open("shapefiles/blockgroups.shp", "rb")) as sf: + with shapefile.Reader( + shp=open(f"{shapefiles_dir.as_posix()}/blockgroups.shp", "rb") + ) as sf: assert len(sf) == 663 shape = sf.shape(3) assert len(shape.points) == 173 @@ -942,7 +956,7 @@ def test_reader_shapefile_delayed_load(): with pytest.raises(shapefile.ShapefileException): sf.shape(0) # assert that works after loading file manually - sf.load("shapefiles/blockgroups") + sf.load(f"{shapefiles_dir.as_posix()}/blockgroups") assert len(sf) == 663 @@ -951,7 +965,7 @@ def test_records_match_shapes(): Assert that the number of records matches the number of shapes in the shapefile. """ - with shapefile.Reader("shapefiles/blockgroups") as sf: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups") as sf: records = sf.records() shapes = sf.shapes() assert len(records) == len(shapes) @@ -965,7 +979,7 @@ def test_record_attributes(fields=None): # note # second element in fields matches first element # in record because records dont have DeletionFlag - with shapefile.Reader("shapefiles/blockgroups") as sf: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups") as sf: for i in range(len(sf)): # full record full_record = sf.record(i) @@ -1028,7 +1042,7 @@ def test_record_subfields_duplicates(): fields = ["AREA", "AREA", "AREA", "MALES", "MALES", "MOBILEHOME"] test_record_attributes(fields=fields) # check that only 3 values - with shapefile.Reader("shapefiles/blockgroups") as sf: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups") as sf: rec = sf.record(0, fields=fields) assert len(rec) == len(set(fields)) @@ -1041,7 +1055,7 @@ def test_record_subfields_empty(): fields = [] test_record_attributes(fields=fields) # check that only 0 values - with shapefile.Reader("shapefiles/blockgroups") as sf: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups") as sf: rec = sf.record(0, fields=fields) assert len(rec) == 0 @@ -1051,7 +1065,7 @@ def test_record_as_dict(): Assert that a record object can be converted into a dictionary and data remains correct. """ - with shapefile.Reader("shapefiles/blockgroups") as sf: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups") as sf: record = sf.record(0) as_dict = record.as_dict() @@ -1065,7 +1079,7 @@ def test_record_oid(): Assert that the record's oid attribute returns its index in the shapefile. """ - with shapefile.Reader("shapefiles/blockgroups") as sf: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups") as sf: for i in range(len(sf)): record = sf.record(i) assert record.oid == i @@ -1088,7 +1102,7 @@ def test_iterRecords_start_stop(): by index with Reader.record """ - with shapefile.Reader("shapefiles/blockgroups") as sf: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups") as sf: N = len(sf) # Arbitrary selection of record indices @@ -1126,7 +1140,7 @@ def test_shape_oid(): Assert that the shape's oid attribute returns its index in the shapefile. """ - with shapefile.Reader("shapefiles/blockgroups") as sf: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups") as sf: for i in range(len(sf)): shape = sf.shape(i) assert shape.oid == i @@ -1146,7 +1160,7 @@ def test_shape_oid_no_shx(): Assert that the shape's oid attribute returns its index in the shapefile, when shx file is missing. """ - basename = "shapefiles/blockgroups" + basename = f"{shapefiles_dir.as_posix()}/blockgroups" shp = open(basename + ".shp", "rb") dbf = open(basename + ".dbf", "rb") with shapefile.Reader(shp=shp, dbf=dbf) as sf: @@ -1182,7 +1196,7 @@ def test_reader_offsets(): Assert that reader will not read the shx offsets unless necessary, i.e. requesting a shape index. """ - basename = "shapefiles/blockgroups" + basename = f"{shapefiles_dir.as_posix()}/blockgroups" with shapefile.Reader(basename) as sf: # shx offsets should not be read during loading assert sf.shx_reader._shxRecords_16bw is None @@ -1196,7 +1210,7 @@ def test_reader_offsets_no_shx(): Assert that reading a shapefile without a shx file will not build the offsets unless necessary, i.e. reading all the shapes. """ - basename = "shapefiles/blockgroups" + basename = f"{shapefiles_dir.as_posix()}/blockgroups" shp = open(basename + ".shp", "rb") dbf = open(basename + ".dbf", "rb") with shapefile.Reader(shp=shp, dbf=dbf) as sf: @@ -1218,7 +1232,7 @@ def test_reader_numshapes(): Assert that reader reads the numShapes attribute from the shx file header during loading. """ - basename = "shapefiles/blockgroups" + basename = f"{shapefiles_dir.as_posix()}/blockgroups" with shapefile.Reader(basename) as sf: # numShapes should be set during loading assert sf.numShapes is not None @@ -1232,7 +1246,7 @@ def test_reader_numshapes_no_shx(): an unknown value for the numShapes attribute (None), and that reading all the shapes will set the numShapes attribute. """ - basename = "shapefiles/blockgroups" + basename = f"{shapefiles_dir.as_posix()}/blockgroups" shp = open(basename + ".shp", "rb") dbf = open(basename + ".dbf", "rb") with shapefile.Reader(shp=shp, dbf=dbf) as sf: @@ -1248,7 +1262,7 @@ def test_reader_len(): Assert that calling len() on reader is equal to length of all shapes and records. """ - basename = "shapefiles/blockgroups" + basename = f"{shapefiles_dir.as_posix()}/blockgroups" with shapefile.Reader(basename) as sf: assert len(sf) == len(sf.records()) == len(sf.shapes()) @@ -1267,7 +1281,7 @@ def test_reader_len_dbf_only(): Assert that calling len() on reader when reading a dbf file only, is equal to length of all records. """ - basename = "shapefiles/blockgroups" + basename = f"{shapefiles_dir.as_posix()}/blockgroups" dbf = open(basename + ".dbf", "rb") with shapefile.Reader(dbf=dbf) as sf: assert len(sf) == len(sf.records()) @@ -1278,7 +1292,7 @@ def test_reader_len_no_dbf(): Assert that calling len() on reader when dbf file is missing, is equal to length of all shapes. """ - basename = "shapefiles/blockgroups" + basename = f"{shapefiles_dir.as_posix()}/blockgroups" shp = open(basename + ".shp", "rb") shx = open(basename + ".shx", "rb") with shapefile.Reader(shp=shp, shx=shx) as sf: @@ -1290,7 +1304,7 @@ def test_reader_len_no_dbf_shx(): Assert that calling len() on reader when dbf and shx file is missing, is equal to length of all shapes. """ - basename = "shapefiles/blockgroups" + basename = f"{shapefiles_dir.as_posix()}/blockgroups" shp = open(basename + ".shp", "rb") with shapefile.Reader(shp=shp) as sf: assert len(sf) == len(sf.shapes()) @@ -1346,7 +1360,7 @@ def test_bboxfilter_shape(): outside = list(inside) outside[0] *= 10 outside[2] *= 10 - with shapefile.Reader("shapefiles/blockgroups") as sf: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups") as sf: assert sf.shape(0, bbox=inside) is not None assert sf.shape(0, bbox=outside) is None @@ -1357,7 +1371,7 @@ def test_bboxfilter_shapes(): that fall outside, and returns those that fall inside. """ bbox = [-122.4, 37.8, -122.35, 37.82] - with shapefile.Reader("shapefiles/blockgroups") as sf: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups") as sf: # apply bbox filter shapes = sf.shapes(bbox=bbox) # manually check bboxes @@ -1379,7 +1393,7 @@ def test_bboxfilter_shapes_outside(): no shapes when the bbox is outside the entire shapefile. """ bbox = [-180, 89, -179, 90] - with shapefile.Reader("shapefiles/blockgroups") as sf: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups") as sf: shapes = sf.shapes(bbox=bbox) assert len(shapes) == 0 @@ -1390,7 +1404,7 @@ def test_bboxfilter_itershapes(): that fall outside, and returns those that fall inside. """ bbox = [-122.4, 37.8, -122.35, 37.82] - with shapefile.Reader("shapefiles/blockgroups") as sf: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups") as sf: # apply bbox filter shapes = list(sf.iterShapes(bbox=bbox)) # manually check bboxes @@ -1415,7 +1429,7 @@ def test_bboxfilter_shaperecord(): outside = list(inside) outside[0] *= 10 outside[2] *= 10 - with shapefile.Reader("shapefiles/blockgroups") as sf: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups") as sf: # inside shaperec = sf.shapeRecord(0, bbox=inside) assert shaperec is not None @@ -1430,7 +1444,7 @@ def test_bboxfilter_shaperecords(): that fall outside, and returns those that fall inside. """ bbox = [-122.4, 37.8, -122.35, 37.82] - with shapefile.Reader("shapefiles/blockgroups") as sf: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups") as sf: # apply bbox filter shaperecs = sf.shapeRecords(bbox=bbox) # manually check bboxes @@ -1458,7 +1472,7 @@ def test_bboxfilter_itershaperecords(): that fall outside, and returns those that fall inside. """ bbox = [-122.4, 37.8, -122.35, 37.82] - with shapefile.Reader("shapefiles/blockgroups") as sf: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups") as sf: # apply bbox filter shaperecs = list(sf.iterShapeRecords(bbox=bbox)) # manually check bboxes @@ -1487,7 +1501,7 @@ def test_shaperecords_shaperecord(): Assert that shapeRecord returns a single ShapeRecord at the given index. """ - with shapefile.Reader("shapefiles/blockgroups") as sf: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups") as sf: shaperecs = sf.shapeRecords() shaperec = sf.shapeRecord(0) should_match = shaperecs[0] @@ -1506,7 +1520,7 @@ def test_shaperecord_shape(): Assert that a ShapeRecord object has a shape attribute that contains shape data. """ - with shapefile.Reader("shapefiles/blockgroups") as sf: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups") as sf: shaperec = sf.shapeRecord(3) shape = shaperec.shape point = shape.points[0] @@ -1518,7 +1532,7 @@ def test_shaperecord_record(): Assert that a ShapeRecord object has a record attribute that contains record data. """ - with shapefile.Reader("shapefiles/blockgroups") as sf: + with shapefile.Reader(f"{shapefiles_dir.as_posix()}/blockgroups") as sf: shaperec = sf.shapeRecord(3) record = shaperec.record @@ -1540,7 +1554,9 @@ def test_reader_zip_polyylinez_no_m_itershaperecords(): Original source: https://github.com/OpenNHM/AvaFrameData/blob/main/avaPopeletzbach/ License CC-BY-4.0 """ - with shapefile.Reader("shapefiles/REL.zip/REL/releaseArea20090407") as sf: + with shapefile.Reader( + f"{shapefiles_dir.as_posix()}/REL.zip/REL/releaseArea20090407" + ) as sf: for _shaperec in sf.iterShapeRecords(): pass @@ -2005,9 +2021,3 @@ def test_write_multipatch(tmpdir): w.record("house1") w.close() - - -# This allows a PyShp wheel installed in the env to be tested -# against the doctests. -if __name__ == "__main__": - shapefile.main()