Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type annotation implementation #203

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion LICENSE.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright © 2007-2019, contributors of geojson
Copyright © 2007-2023, contributors of geojson

All rights reserved.

Expand Down
29 changes: 21 additions & 8 deletions geojson/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__",
)
109 changes: 65 additions & 44 deletions geojson/base.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,50 @@
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:
return 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.

Expand All @@ -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

Expand All @@ -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.

Expand All @@ -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__)
62 changes: 42 additions & 20 deletions geojson/codec.py
Original file line number Diff line number Diff line change
@@ -1,53 +1,75 @@
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


# Wrap the functions from json, providing encoder, decoders, and
# 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,
Expand Down
25 changes: 19 additions & 6 deletions geojson/examples.py
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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,
Expand All @@ -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
Expand Down
17 changes: 12 additions & 5 deletions geojson/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)
Loading