From 56bf29c5995c2eed66ae1a80b17f45aa16e1c9c7 Mon Sep 17 00:00:00 2001 From: Aleksei Semenov Date: Tue, 28 Mar 2023 13:04:26 -0300 Subject: [PATCH 1/2] Make minor changes that don't affect functionality Fix bugs with wildcard imports Use context manager for file reading in `setup.py` Update tox.ini Update license copyright years --- LICENSE.rst | 2 +- geojson/__init__.py | 29 +++++++++++++++++++++-------- geojson/factory.py | 17 ++++++++++++----- setup.py | 17 +++++++++-------- tox.ini | 12 +++++++----- 5 files changed, 50 insertions(+), 27 deletions(-) diff --git a/LICENSE.rst b/LICENSE.rst index 664bb61..65a0c9c 100644 --- a/LICENSE.rst +++ b/LICENSE.rst @@ -1,4 +1,4 @@ -Copyright © 2007-2019, contributors of geojson +Copyright © 2007-2023, contributors of geojson All rights reserved. diff --git a/geojson/__init__.py b/geojson/__init__.py index 18d1532..ee67e04 100644 --- a/geojson/__init__.py +++ b/geojson/__init__.py @@ -7,11 +7,24 @@ from geojson.base import GeoJSON from geojson._version import __version__, __version_info__ -__all__ = ([dump, dumps, load, loads, GeoJSONEncoder] + - [coords, map_coords] + - [Point, LineString, Polygon] + - [MultiLineString, MultiPoint, MultiPolygon] + - [GeometryCollection] + - [Feature, FeatureCollection] + - [GeoJSON] + - [__version__, __version_info__]) +__all__ = ( + "dump", + "dumps", + "load", + "loads", + "GeoJSONEncoder", + "coords", + "map_coords", + "Point", + "LineString", + "Polygon", + "MultiLineString", + "MultiPoint", + "MultiPolygon", + "GeometryCollection", + "Feature", + "FeatureCollection", + "GeoJSON", + "__version__", + "__version_info__", +) diff --git a/geojson/factory.py b/geojson/factory.py index b8090bb..8d99a0e 100644 --- a/geojson/factory.py +++ b/geojson/factory.py @@ -4,8 +4,15 @@ from geojson.feature import Feature, FeatureCollection from geojson.base import GeoJSON -__all__ = ([Point, LineString, Polygon] + - [MultiLineString, MultiPoint, MultiPolygon] + - [GeometryCollection] + - [Feature, FeatureCollection] + - [GeoJSON]) +__all__ = ( + "Point", + "LineString", + "Polygon", + "MultiLineString", + "MultiPoint", + "MultiPolygon", + "GeometryCollection", + "Feature", + "FeatureCollection", + "GeoJSON", +) diff --git a/setup.py b/setup.py index 52c38e7..a6731c8 100644 --- a/setup.py +++ b/setup.py @@ -6,14 +6,15 @@ with open("README.rst") as readme_file: readme_text = readme_file.read() -VERSIONFILE = "geojson/_version.py" -verstrline = open(VERSIONFILE).read() -VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]" -mo = re.search(VSRE, verstrline, re.M) -if mo: - verstr = mo.group(1) -else: - raise RuntimeError(f"Unable to find version string in {VERSIONFILE}.") +VERSIONFILE_PATH = "geojson/_version.py" +with open(VERSIONFILE_PATH) as version_file: + verstrline = version_file.read() + VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]" + mo = re.search(VSRE, verstrline, re.M) + if mo: + verstr = mo.group(1) + else: + raise RuntimeError(f"Unable to find version string in {VERSIONFILE_PATH}.") def test_suite(): diff --git a/tox.ini b/tox.ini index 1209cbf..5c2b820 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,12 @@ -# Tox (http://tox.testrun.org/) is a tool for running tests -# in multiple virtualenvs. This configuration file will run the -# test suite on all supported python versions. To use it, "pip install tox" -# and then run "tox" from this directory. +# Tox (https://tox.wiki/) is a tool for running tests in multiple virtualenvs. +# This configuration file will run the test suite on all supported python +# versions. To use it install tox with `pip install tox` and then run `tox` +# from this directory. [tox] -envlist = py{36,37,38,39,310}, pypy3 +envlist = py{37,38,39,310,311}, pypy3 [testenv] +description = Run unittests and doctests +skip_install = true commands = {envpython} setup.py test From b7cc078c1c10e5041d3ac8885b152e44fc5ea5ba Mon Sep 17 00:00:00 2001 From: Aleksei Semenov Date: Fri, 7 Apr 2023 10:49:15 -0300 Subject: [PATCH 2/2] Implement type annotations Add module `types.py` with custom types Add type annotations to geojson functions Refactor some functions Update functions docstrings --- geojson/base.py | 109 ++++++++++++++++++++++++++------------------ geojson/codec.py | 62 +++++++++++++++++-------- geojson/examples.py | 25 +++++++--- geojson/feature.py | 29 ++++++++---- geojson/geometry.py | 62 +++++++++++++++++-------- geojson/mapping.py | 28 ++++++++---- geojson/types.py | 37 +++++++++++++++ geojson/utils.py | 72 +++++++++++++++++++---------- setup.py | 11 +++-- 9 files changed, 300 insertions(+), 135 deletions(-) create mode 100644 geojson/types.py diff --git a/geojson/base.py b/geojson/base.py index 37a316e..c812842 100644 --- a/geojson/base.py +++ b/geojson/base.py @@ -1,37 +1,42 @@ +from __future__ import annotations + +from typing import Any, Callable, Iterable, Optional, Type, Union, TYPE_CHECKING + import geojson from geojson.mapping import to_mapping +if TYPE_CHECKING: + from geojson.types import G, LineLike, PolyLike, ErrorList, CheckErrorFunc + class GeoJSON(dict): """ A class representing a GeoJSON object. """ - def __init__(self, iterable=(), **extra): + def __init__(self, iterable: Iterable[Any] = (), **extra) -> None: """ - Initialises a GeoJSON object + Initializes a GeoJSON object :param iterable: iterable from which to draw the content of the GeoJSON object. :type iterable: dict, array, tuple - :return: a GeoJSON object - :rtype: GeoJSON """ super().__init__(iterable) self["type"] = getattr(self, "type", type(self).__name__) self.update(extra) - def __repr__(self): + def __repr__(self) -> str: return geojson.dumps(self, sort_keys=True) __str__ = __repr__ - def __getattr__(self, name): + def __getattr__(self, name: str) -> Any: """ Permit dictionary items to be retrieved like object attributes :param name: attribute name - :type name: str, int + :type name: str :return: dictionary value """ try: @@ -39,7 +44,7 @@ def __getattr__(self, name): except KeyError: raise AttributeError(name) - def __setattr__(self, name, value): + def __setattr__(self, name: str, value: Any) -> None: """ Permit dictionary items to be set like object attributes. @@ -50,7 +55,7 @@ def __setattr__(self, name, value): self[name] = value - def __delattr__(self, name): + def __delattr__(self, name: str) -> None: """ Permit dictionary items to be deleted like object attributes @@ -61,12 +66,19 @@ def __delattr__(self, name): del self[name] @property - def __geo_interface__(self): - if self.type != "GeoJSON": + def __geo_interface__(self) -> Optional[G]: + if self.type == "GeoJSON": + return None + else: return self @classmethod - def to_instance(cls, ob, default=None, strict=False): + def to_instance( + cls, + ob: Optional[G], + default: Union[Type[GeoJSON], Callable[..., G], None] = None, + strict: bool = False, + ) -> G: """Encode a GeoJSON dict into an GeoJSON object. Assumes the caller knows that the dict should satisfy a GeoJSON type. @@ -91,49 +103,58 @@ def to_instance(cls, ob, default=None, strict=False): :raises AttributeError: If the input dict contains items that are not valid GeoJSON types. """ - if ob is None and default is not None: - instance = default() - elif isinstance(ob, GeoJSON): - instance = ob - else: - mapping = to_mapping(ob) - d = {} - for k in mapping: - d[k] = mapping[k] - try: - type_ = d.pop("type") - try: - type_ = str(type_) - except UnicodeEncodeError: - # If the type contains non-ascii characters, we can assume - # it's not a valid GeoJSON type - raise AttributeError( - "{0} is not a GeoJSON type").format(type_) - geojson_factory = getattr(geojson.factory, type_) - instance = geojson_factory(**d) - except (AttributeError, KeyError) as invalid: - if strict: - msg = "Cannot coerce %r into a valid GeoJSON structure: %s" - msg %= (ob, invalid) - raise ValueError(msg) - instance = ob - return instance + if ob is None: + if default is None: + raise ValueError("At least one argument must be provided") + else: + return default() + + if isinstance(ob, GeoJSON): + return ob + + # If object is not an instance of GeoJSON + mapping = to_mapping(ob) + d = {k: v for k, v in mapping.items()} + error_msg = "Cannot coerce %r into a valid GeoJSON structure: %s" + try: + type_ = d.pop("type") + except KeyError as invalid: + if strict: + raise ValueError(error_msg.format(ob, invalid)) + return ob + try: + type_ = str(type_) + except UnicodeEncodeError: + # If the type contains non-ascii characters, we can assume + # it's not a valid GeoJSON type + raise AttributeError(f"{type_} is not a GeoJSON type") + try: + geojson_factory: Type[GeoJSON] = getattr(geojson.factory, type_) + return geojson_factory(**d) + except (AttributeError, TypeError) as invalid: + if strict: + raise ValueError(error_msg.format(ob, invalid)) + return ob @property - def is_valid(self): + def is_valid(self) -> bool: return not self.errors() - def check_list_errors(self, checkFunc, lst): + def check_list_errors( + self, + checkFunc: CheckErrorFunc, + lst: Union[LineLike, PolyLike], + ) -> ErrorList: """Validation helper function.""" - # check for errors on each subitem, filter only subitems with errors + # Check for errors on each subitem, filter only subitems with errors results = (checkFunc(i) for i in lst) return [err for err in results if err] - def errors(self): + def errors(self) -> Any: """Return validation errors (if any). Implement in each subclass. """ - # make sure that each subclass implements it's own validation function + # Make sure that each subclass implements it's own validation function if self.__class__ != GeoJSON: raise NotImplementedError(self.__class__) diff --git a/geojson/codec.py b/geojson/codec.py index 00ada79..8443f4a 100644 --- a/geojson/codec.py +++ b/geojson/codec.py @@ -1,16 +1,21 @@ +from __future__ import annotations + try: import simplejson as json except ImportError: import json -import geojson +from typing import TYPE_CHECKING, IO, Any, Callable, Type + import geojson.factory from geojson.mapping import to_mapping +if TYPE_CHECKING: + from geojson.types import G -class GeoJSONEncoder(json.JSONEncoder): - def default(self, obj): +class GeoJSONEncoder(json.JSONEncoder): + def default(self, obj: G) -> G: return geojson.factory.GeoJSON.to_instance(obj) # NOQA @@ -18,36 +23,53 @@ def default(self, obj): # object creation hooks. # Here the defaults are set to only permit valid JSON as per RFC 4267 -def _enforce_strict_numbers(obj): +def _enforce_strict_numbers(obj: str) -> None: raise ValueError("Number %r is not JSON compliant" % obj) -def dump(obj, fp, cls=GeoJSONEncoder, allow_nan=False, **kwargs): +def dump( + obj: G, + fp: IO[str], + cls: Type[json.JSONEncoder] = GeoJSONEncoder, + allow_nan: bool = False, + **kwargs +) -> None: return json.dump(to_mapping(obj), fp, cls=cls, allow_nan=allow_nan, **kwargs) -def dumps(obj, cls=GeoJSONEncoder, allow_nan=False, ensure_ascii=False, **kwargs): - return json.dumps(to_mapping(obj), - cls=cls, allow_nan=allow_nan, ensure_ascii=ensure_ascii, **kwargs) - - -def load(fp, - cls=json.JSONDecoder, - parse_constant=_enforce_strict_numbers, - object_hook=geojson.base.GeoJSON.to_instance, - **kwargs): +def dumps( + obj: G, + cls: Type[json.JSONEncoder] = GeoJSONEncoder, + allow_nan: bool = False, + ensure_ascii: bool = False, + **kwargs +) -> str: + return json.dumps( + to_mapping(obj), + cls=cls, + allow_nan=allow_nan, + ensure_ascii=ensure_ascii, + **kwargs + ) + + +def load(fp: IO[str], + cls: Type[json.JSONDecoder] = json.JSONDecoder, + parse_constant: Callable[[str], None] = _enforce_strict_numbers, + object_hook: Callable[..., G] = geojson.factory.GeoJSON.to_instance, + **kwargs) -> Any: return json.load(fp, cls=cls, object_hook=object_hook, parse_constant=parse_constant, **kwargs) -def loads(s, - cls=json.JSONDecoder, - parse_constant=_enforce_strict_numbers, - object_hook=geojson.base.GeoJSON.to_instance, - **kwargs): +def loads(s: str, + cls: Type[json.JSONDecoder] = json.JSONDecoder, + parse_constant: Callable[[str], None] = _enforce_strict_numbers, + object_hook: Callable[..., G] = geojson.factory.GeoJSON.to_instance, + **kwargs) -> Any: return json.loads(s, cls=cls, object_hook=object_hook, parse_constant=parse_constant, diff --git a/geojson/examples.py b/geojson/examples.py index b2f29cf..b7325d6 100644 --- a/geojson/examples.py +++ b/geojson/examples.py @@ -1,11 +1,26 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, MutableMapping, Optional, Union + +if TYPE_CHECKING: + from geojson.geometry import Geometry + from geojson.types import G + + class SimpleWebFeature: """ A simple, Atom-ish, single geometry (WGS84) GIS feature. """ - def __init__(self, id=None, geometry=None, title=None, summary=None, - link=None): + def __init__( + self, + id: Optional[Any] = None, + geometry: Union[Geometry, MutableMapping, None] = None, + title: Optional[str] = None, + summary: Optional[str] = None, + link: Optional[str] = None, + ) -> None: """ Initialises a SimpleWebFeature from the parameters provided. @@ -19,14 +34,12 @@ def __init__(self, id=None, geometry=None, title=None, summary=None, :type summary: str :param link: Link associated with the object. :type link: str - :return: A SimpleWebFeature object - :rtype: SimpleWebFeature """ self.id = id self.geometry = geometry self.properties = {'title': title, 'summary': summary, 'link': link} - def as_dict(self): + def as_dict(self) -> dict[str, Any]: return { "type": "Feature", "id": self.id, @@ -43,7 +56,7 @@ def as_dict(self): """ -def create_simple_web_feature(o): +def create_simple_web_feature(o: G) -> Union[SimpleWebFeature, G]: """ Create an instance of SimpleWebFeature from a dict, o. If o does not match a Python feature object, simply return o. This function serves as a diff --git a/geojson/feature.py b/geojson/feature.py index 06e3e60..26646d6 100644 --- a/geojson/feature.py +++ b/geojson/feature.py @@ -2,16 +2,29 @@ SimpleWebFeature is a working example of a class that satisfies the Python geo interface. """ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union from geojson.base import GeoJSON +if TYPE_CHECKING: + from geojson.geometry import Geometry + from geojson.types import ErrorList, ErrorSingle + class Feature(GeoJSON): """ Represents a WGS84 GIS feature. """ - def __init__(self, id=None, geometry=None, properties=None, **extra): + def __init__( + self, + id: Optional[Any] = None, + geometry: Optional[Geometry] = None, + properties: Optional[Dict[str, Any]] = None, + **extra + ) -> None: """ Initialises a Feature object with the given parameters. @@ -20,8 +33,6 @@ def __init__(self, id=None, geometry=None, properties=None, **extra): :param geometry: Geometry corresponding to the feature. :param properties: Dict containing properties of the feature. :type properties: dict - :return: Feature object - :rtype: Feature """ super().__init__(**extra) if id is not None: @@ -30,8 +41,8 @@ def __init__(self, id=None, geometry=None, properties=None, **extra): if geometry else None) self["properties"] = properties or {} - def errors(self): - geo = self.get('geometry') + def errors(self) -> Union[ErrorSingle, ErrorList]: + geo: Optional[Geometry] = self.get('geometry') return geo.errors() if geo else None @@ -40,21 +51,19 @@ class FeatureCollection(GeoJSON): Represents a FeatureCollection, a set of multiple Feature objects. """ - def __init__(self, features, **extra): + def __init__(self, features: List[Feature], **extra) -> None: """ Initialises a FeatureCollection object from the :param features: List of features to constitute the FeatureCollection. :type features: list - :return: FeatureCollection object - :rtype: FeatureCollection """ super().__init__(**extra) self["features"] = features - def errors(self): + def errors(self) -> ErrorList: return self.check_list_errors(lambda x: x.errors(), self.features) - def __getitem__(self, key): + def __getitem__(self, key: str) -> Any: try: return self.get("features", ())[key] except (KeyError, TypeError, IndexError): diff --git a/geojson/geometry.py b/geojson/geometry.py index f22db60..c581612 100755 --- a/geojson/geometry.py +++ b/geojson/geometry.py @@ -1,8 +1,20 @@ +from __future__ import annotations + from decimal import Decimal -from numbers import Number, Real +from numbers import Real +from typing import TYPE_CHECKING, Any, List, Optional from geojson.base import GeoJSON +if TYPE_CHECKING: + from geojson.types import ( + CoordsAny, + PointLike, + LineLike, + ErrorList, + ErrorSingle, + ) + DEFAULT_PRECISION = 6 @@ -12,7 +24,13 @@ class Geometry(GeoJSON): Represents an abstract base class for a WGS84 geometry. """ - def __init__(self, coordinates=None, validate=False, precision=None, **extra): + def __init__( + self, + coordinates: Optional[CoordsAny] = None, + validate: bool = False, + precision: Optional[int] = None, + **extra + ) -> None: """ Initialises a Geometry object. @@ -35,11 +53,10 @@ def __init__(self, coordinates=None, validate=False, precision=None, **extra): raise ValueError(f'{errors}: {coordinates}') @classmethod - def clean_coordinates(cls, coords, precision): + def clean_coordinates(cls, coords: CoordsAny, precision: int) -> CoordsAny: if isinstance(coords, cls): return coords['coordinates'] - - new_coords = [] + new_coords: List[CoordsAny] = [] if isinstance(coords, Geometry): coords = [coords] for coord in coords: @@ -59,15 +76,17 @@ class GeometryCollection(GeoJSON): Represents an abstract base class for collections of WGS84 geometries. """ - def __init__(self, geometries=None, **extra): + def __init__( + self, geometries: Optional[List[Geometry]] = None, **extra + ) -> None: super().__init__(**extra) self["geometries"] = geometries or [] - def errors(self): + def errors(self) -> List[str]: errors = [geom.errors() for geom in self['geometries']] return [err for err in errors if err] - def __getitem__(self, key): + def __getitem__(self, key: str) -> Any: try: return self.get("geometries", ())[key] except (KeyError, TypeError, IndexError): @@ -76,27 +95,28 @@ def __getitem__(self, key): # Marker classes. -def check_point(coord): +def check_point(coord: PointLike) -> ErrorSingle: if not isinstance(coord, list): return 'each position must be a list' if len(coord) not in (2, 3): return 'a position must have exactly 2 or 3 values' for number in coord: - if not isinstance(number, Number): + if not isinstance(number, (Real, Decimal)): return 'a position cannot have inner positions' + return None class Point(Geometry): - def errors(self): + def errors(self) -> ErrorSingle: return check_point(self['coordinates']) class MultiPoint(Geometry): - def errors(self): + def errors(self) -> ErrorList: return self.check_list_errors(check_point, self['coordinates']) -def check_line_string(coord): +def check_line_string(coord: LineLike) -> ErrorSingle: if not isinstance(coord, list): return 'each line must be a list of positions' if len(coord) < 2: @@ -106,19 +126,21 @@ def check_line_string(coord): error = check_point(pos) if error: return error + return None class LineString(MultiPoint): - def errors(self): - return check_line_string(self['coordinates']) + def errors(self) -> ErrorList: + error = check_line_string(self['coordinates']) + return [error] if error else [] class MultiLineString(Geometry): - def errors(self): + def errors(self) -> ErrorList: return self.check_list_errors(check_line_string, self['coordinates']) -def check_polygon(coord): +def check_polygon(coord: LineLike) -> ErrorSingle: if not isinstance(coord, list): return 'Each polygon must be a list of linear rings' @@ -133,14 +155,16 @@ def check_polygon(coord): if isring is False: return 'Each linear ring must end where it started' + return None + class Polygon(Geometry): - def errors(self): + def errors(self) -> ErrorSingle: return check_polygon(self['coordinates']) class MultiPolygon(Geometry): - def errors(self): + def errors(self) -> ErrorList: return self.check_list_errors(check_polygon, self['coordinates']) diff --git a/geojson/mapping.py b/geojson/mapping.py index 1ceffbc..17609e5 100644 --- a/geojson/mapping.py +++ b/geojson/mapping.py @@ -1,19 +1,24 @@ -from collections.abc import MutableMapping +from __future__ import annotations try: import simplejson as json except ImportError: import json -import geojson +from typing import TYPE_CHECKING, Any, Optional, Union, MutableMapping + +if TYPE_CHECKING: + from geojson.base import GeoJSON + from geojson.types import G GEO_INTERFACE_MARKER = "__geo_interface__" -def is_mapping(obj): +def is_mapping(obj: Any) -> bool: """ Checks if the object is an instance of MutableMapping. + Exists for backwards compatibility only. :param obj: Object to be checked. :return: Truth value of whether the object is an instance of @@ -23,17 +28,24 @@ def is_mapping(obj): return isinstance(obj, MutableMapping) -def to_mapping(obj): +def to_mapping(obj: Union[G, str]) -> G: + """ + Converts an object to a GeoJSON-like object. - mapping = getattr(obj, GEO_INTERFACE_MARKER, None) + :param obj: A GeoJSON-like object, or a JSON string. + :type obj: GeoJSON-like object, str + :return: A dictionary-like object representing the input GeoJSON. + :rtype: GeoJSON-like object + """ + mapping: Optional[GeoJSON] = getattr(obj, GEO_INTERFACE_MARKER, None) if mapping is not None: return mapping - if is_mapping(obj): + if isinstance(obj, MutableMapping): return obj - if isinstance(obj, geojson.GeoJSON): - return dict(obj) + if isinstance(obj, str): + return json.loads(obj) return json.loads(json.dumps(obj)) diff --git a/geojson/types.py b/geojson/types.py new file mode 100644 index 0000000..63df7cc --- /dev/null +++ b/geojson/types.py @@ -0,0 +1,37 @@ +from decimal import Decimal +from numbers import Real +from typing import ( + Callable, + List, + MutableMapping, + Optional, + Tuple, + TypeAlias, + TypeVar, + Union, +) + +from geojson.base import GeoJSON + +# Python 3.7 doesn't support using isinstance() with Protocols +# Use checks for (Real, Decimal) instead +SupportsRound: TypeAlias = Union[Real, Decimal] + +# Annotation for GeoJSON-like objects +# Multiple types used, because mutable collections are invariant +G = TypeVar('G', MutableMapping, dict, GeoJSON) + +# Annotations for list-like containers (not GeoJSON instances) +PointLike: TypeAlias = Union[List[SupportsRound], Tuple[SupportsRound, ...]] +LineLike: TypeAlias = List[PointLike] +PolyLike: TypeAlias = List[LineLike] +MultiPolyLike: TypeAlias = List[PolyLike] +CoordsAny: TypeAlias = Union[ + PointLike, LineLike, PolyLike, MultiPolyLike, G, List[G] +] + +# Annotations for GeoJSON validations +CheckValue = TypeVar('CheckValue', PointLike, LineLike) +ErrorSingle: TypeAlias = Optional[str] +ErrorList: TypeAlias = List[ErrorSingle] +CheckErrorFunc: TypeAlias = Callable[[CheckValue], ErrorSingle] diff --git a/geojson/utils.py b/geojson/utils.py index 5fb5f2e..74db1a4 100644 --- a/geojson/utils.py +++ b/geojson/utils.py @@ -1,7 +1,25 @@ """Coordinate utility functions.""" +from __future__ import annotations +from decimal import Decimal +from numbers import Real +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Generator, + List, + Tuple, + Union, +) -def coords(obj): +from geojson.factory import LineString, Point, Polygon + +if TYPE_CHECKING: + from geojson.types import CoordsAny, G, PointLike, SupportsRound + + +def coords(obj: Any) -> Generator[Tuple[SupportsRound, ...], None, None]: """ Yields the coordinates from a Feature or Geometry. @@ -25,14 +43,14 @@ def coords(obj): else: coordinates = obj.get('coordinates', obj) for e in coordinates: - if isinstance(e, (float, int)): + if isinstance(e, (Real, Decimal)): yield tuple(coordinates) break for f in coords(e): yield f -def map_coords(func, obj): +def map_coords(func: Callable, obj: G) -> Union[G, dict]: """ Returns the mapped coordinates from a Geometry after applying the provided function to each dimension in tuples list (ie, linear scaling). @@ -45,17 +63,17 @@ def map_coords(func, obj): MultiPolygon :return: The result of applying the function to each dimension in the array. - :rtype: list + :rtype: dict :raises ValueError: if the provided object is not GeoJSON. """ - def tuple_func(coord): + def tuple_func(coord: PointLike) -> PointLike: return (func(coord[0]), func(coord[1])) return map_tuples(tuple_func, obj) -def map_tuples(func, obj): +def map_tuples(func: Callable, obj: G) -> Union[G, dict]: """ Returns the mapped coordinates from a Geometry after applying the provided function to each coordinate. @@ -67,10 +85,10 @@ def map_tuples(func, obj): MultiPolygon :return: The result of applying the function to each dimension in the array. - :rtype: list + :rtype: dict :raises ValueError: if the provided object is not GeoJSON. """ - + coordinates: CoordsAny if obj['type'] == 'Point': coordinates = tuple(func(obj['coordinates'])) elif obj['type'] in ['LineString', 'MultiPoint']: @@ -91,7 +109,7 @@ def map_tuples(func, obj): return {'type': obj['type'], 'coordinates': coordinates} -def map_geometries(func, obj): +def map_geometries(func: Callable, obj: G) -> Union[G, dict]: """ Returns the result of passing every geometry in the given geojson object through func. @@ -101,7 +119,7 @@ def map_geometries(func, obj): :param obj: A geometry or feature to extract the coordinates from. :type obj: GeoJSON :return: The result of applying the function to each geometry - :rtype: list + :rtype: dict :raises ValueError: if the provided object is not geojson. """ simple_types = [ @@ -128,8 +146,11 @@ def map_geometries(func, obj): raise ValueError("Invalid GeoJSON object %s" % repr(obj)) -def generate_random(featureType, numberVertices=3, - boundingBox=[-180.0, -90.0, 180.0, 90.0]): +def generate_random( + featureType: str, + numberVertices: int = 3, + boundingBox: List[float] = [-180.0, -90.0, 180.0, 90.0], +) -> Union[Point, LineString, Polygon]: """ Generates random geojson features depending on the parameters passed through. @@ -148,29 +169,28 @@ def generate_random(featureType, numberVertices=3, :raises ValueError: if there is no featureType provided. """ - from geojson import Point, LineString, Polygon - import random import math + import random lonMin = boundingBox[0] lonMax = boundingBox[2] - def randomLon(): + def randomLon() -> float: return random.uniform(lonMin, lonMax) latMin = boundingBox[1] latMax = boundingBox[3] - def randomLat(): + def randomLat() -> float: return random.uniform(latMin, latMax) - def createPoint(): + def createPoint() -> Point: return Point((randomLon(), randomLat())) - def createLine(): + def createLine() -> LineString: return LineString([createPoint() for unused in range(numberVertices)]) - def createPoly(): + def createPoly() -> Polygon: aveRadius = 60 ctrX = 0.1 ctrY = 0.2 @@ -180,7 +200,7 @@ def createPoly(): angleSteps = [] lower = (2 * math.pi / numberVertices) - irregularity upper = (2 * math.pi / numberVertices) + irregularity - sum = 0 + sum = float(0) for i in range(numberVertices): tmp = random.uniform(lower, upper) angleSteps.append(tmp) @@ -208,12 +228,12 @@ def createPoly(): points.append(firstVal) return Polygon([points]) - def clip(x, min, max): - if(min > max): + def clip(x: float, min: float, max: float) -> float: + if (min > max): return x - elif(x < min): + elif (x < min): return min - elif(x > max): + elif (x > max): return max else: return x @@ -226,3 +246,7 @@ def clip(x, min, max): if featureType == 'Polygon': return createPoly() + + raise ValueError( + f'Got featureType: {featureType}. Expected: Point, LineString, Polygon' + ) diff --git a/setup.py b/setup.py index a6731c8..a84cee7 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,9 @@ if mo: verstr = mo.group(1) else: - raise RuntimeError(f"Unable to find version string in {VERSIONFILE_PATH}.") + raise RuntimeError( + f"Unable to find version string in {VERSIONFILE_PATH}." + ) def test_suite(): @@ -28,8 +30,9 @@ def test_suite(): major_version, minor_version = sys.version_info[:2] if not (major_version == 3 and 7 <= minor_version <= 11): - sys.stderr.write("Sorry, only Python 3.7 - 3.11 are " - "supported at this time.\n") + sys.stderr.write( + "Sorry, only Python 3.7 - 3.11 are " "supported at this time.\n" + ) exit(1) setup( @@ -64,5 +67,5 @@ def test_suite(): "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Topic :: Scientific/Engineering :: GIS", - ] + ], )