Solve all issues detected by PyCharm

- as of September 2021, PyCharm is used to write some of the code
- PyCharm's built-in code styler, linter, and type checker issued
  some warnings that are resolved in this commit
  + spelling mistakes
  + all instance attributes must be specified explicitly
    in a class's __init__() method
    => use `functools.cached_property` for caching
  + make `tuple`s explicit with `(...)`
  + one test failed randomly although everything is ok
    => adjust the fixture's return value (stub for Google Directions API)
  + reformulate SQL so that PyCharm can understand the symbols
This commit is contained in:
Alexander Hess 2021-09-08 12:07:44 +02:00
commit 1c19da2f70
Signed by: alexander
GPG key ID: 344EA5AB10D868E0
19 changed files with 136 additions and 151 deletions

View file

@ -2,6 +2,7 @@
from __future__ import annotations
import functools
from typing import Any
import folium
@ -80,9 +81,6 @@ class Address(meta.Base):
)
pixels = orm.relationship('AddressPixelAssociation', back_populates='address')
# We do not implement a `.__init__()` method and leave that to SQLAlchemy.
# Instead, we use `hasattr()` to check for uninitialized attributes. grep:b1f68d24
def __repr__(self) -> str:
"""Non-literal text representation."""
return '<{cls}({street} in {city})>'.format(
@ -100,7 +98,7 @@ class Address(meta.Base):
"""
return self.id == self.primary_id
@property
@functools.cached_property
def location(self) -> utils.Location:
"""The location of the address.
@ -112,10 +110,9 @@ class Address(meta.Base):
Implementation detail: This property is cached as none of the
underlying attributes to calculate the value are to be changed.
"""
if not hasattr(self, '_location'): # noqa:WPS421 note:b1f68d24
self._location = utils.Location(self.latitude, self.longitude)
self._location.relate_to(self.city.southwest)
return self._location
location = utils.Location(self.latitude, self.longitude)
location.relate_to(self.city.southwest)
return location
@property
def x(self) -> int: # noqa=WPS111

View file

@ -2,6 +2,7 @@
from __future__ import annotations
import functools
import itertools
import json
from typing import List
@ -46,7 +47,7 @@ class DistanceMatrix(meta.Base):
# The duration is measured in seconds.
bicycle_duration = sa.Column(sa.Integer, nullable=True)
# An array of latitude-longitude pairs approximating a courier's way.
directions = sa.Column(postgresql.JSON, nullable=True)
_directions = sa.Column('directions', postgresql.JSON, nullable=True)
# Constraints
__table_args__ = (
@ -73,7 +74,7 @@ class DistanceMatrix(meta.Base):
'0 <= air_distance AND air_distance < 20000', name='realistic_air_distance',
),
sa.CheckConstraint(
'bicycle_distance < 25000', # `.bicycle_distance` may not be negatative
'bicycle_distance < 25000', # `.bicycle_distance` may not be negative
name='realistic_bicycle_distance', # due to the constraint below.
),
sa.CheckConstraint(
@ -97,9 +98,6 @@ class DistanceMatrix(meta.Base):
foreign_keys='[DistanceMatrix.second_address_id, DistanceMatrix.city_id]',
)
# We do not implement a `.__init__()` method and leave that to SQLAlchemy.
# Instead, we use `hasattr()` to check for uninitialized attributes. grep:86ffc14e
@classmethod
def from_addresses(
cls, *addresses: db.Address, google_maps: bool = False,
@ -114,7 +112,7 @@ class DistanceMatrix(meta.Base):
Args:
*addresses: to calculate the pair-wise distances for;
must contain at least two `Address` objects
google_maps: if `.bicylce_distance` and `.directions` should be
google_maps: if `.bicycle_distance` and `._directions` should be
populated with a query to the Google Maps Directions API;
by default, only the `.air_distance` is calculated with `geopy`
@ -130,7 +128,7 @@ class DistanceMatrix(meta.Base):
(first, second) if first.id < second.id else (second, first)
)
# If there is no `DistaneMatrix` object in the database ...
# If there is no `DistanceMatrix` object in the database ...
distance = ( # noqa:ECE001
db.session.query(db.DistanceMatrix)
.filter(db.DistanceMatrix.first_address == first)
@ -161,10 +159,10 @@ class DistanceMatrix(meta.Base):
return distances
def sync_with_google_maps(self) -> None:
"""Fill in `.bicycle_distance` and `.directions` with Google Maps.
"""Fill in `.bicycle_distance` and `._directions` with Google Maps.
`.directions` will not contain the coordinates of `.first_address` and
`.second_address`.
`._directions` will NOT contain the coordinates
of `.first_address` and `.second_address`.
This uses the Google Maps Directions API.
@ -207,28 +205,24 @@ class DistanceMatrix(meta.Base):
steps.discard(self.first_address.location.lat_lng)
steps.discard(self.second_address.location.lat_lng)
self.directions = json.dumps(list(steps)) # noqa:WPS601
self._directions = json.dumps(list(steps)) # noqa:WPS601
db.session.add(self)
db.session.commit()
@property
@functools.cached_property
def path(self) -> List[utils.Location]:
"""The couriers' path from `.first_address` to `.second_address`.
The returned `Location`s all relates to `.first_address.city.southwest`.
Implementation detail: This property is cached as none of the
underlying attributes (i.e., `.directions`) are to be changed.
underlying attributes (i.e., `._directions`) are to be changed.
"""
if not hasattr(self, '_path'): # noqa:WPS421 note:86ffc14e
inner_points = [
utils.Location(point[0], point[1])
for point in json.loads(self.directions)
]
for point in inner_points:
point.relate_to(self.first_address.city.southwest)
inner_points = [
utils.Location(*point) for point in json.loads(self._directions)
]
for point in inner_points:
point.relate_to(self.first_address.city.southwest)
self._path = inner_points
return self._path
return inner_points

View file

@ -2,6 +2,8 @@
from __future__ import annotations
import functools
import folium
import sqlalchemy as sa
from sqlalchemy import orm
@ -38,51 +40,39 @@ class City(meta.Base):
addresses = orm.relationship('Address', back_populates='city')
grids = orm.relationship('Grid', back_populates='city')
# We do not implement a `.__init__()` method and leave that to SQLAlchemy.
# Instead, we use `hasattr()` to check for uninitialized attributes. grep:d334120e
# We do not implement a `.__init__()` method and use SQLAlchemy's default.
# The uninitialized attribute `._map` is computed on the fly. note:d334120ei
def __repr__(self) -> str:
"""Non-literal text representation."""
return '<{cls}({name})>'.format(cls=self.__class__.__name__, name=self.name)
@property
@functools.cached_property
def center(self) -> utils.Location:
"""Location of the city's center.
Implementation detail: This property is cached as none of the
underlying attributes to calculate the value are to be changed.
"""
if not hasattr(self, '_center'): # noqa:WPS421 note:d334120e
self._center = utils.Location(self.center_latitude, self.center_longitude)
return self._center
return utils.Location(self.center_latitude, self.center_longitude)
@property
@functools.cached_property
def northeast(self) -> utils.Location:
"""The city's northeast corner of the Google Maps viewport.
Implementation detail: This property is cached as none of the
underlying attributes to calculate the value are to be changed.
"""
if not hasattr(self, '_northeast'): # noqa:WPS421 note:d334120e
self._northeast = utils.Location(
self.northeast_latitude, self.northeast_longitude,
)
return utils.Location(self.northeast_latitude, self.northeast_longitude)
return self._northeast
@property
@functools.cached_property
def southwest(self) -> utils.Location:
"""The city's southwest corner of the Google Maps viewport.
Implementation detail: This property is cached as none of the
underlying attributes to calculate the value are to be changed.
"""
if not hasattr(self, '_southwest'): # noqa:WPS421 note:d334120e
self._southwest = utils.Location(
self.southwest_latitude, self.southwest_longitude,
)
return self._southwest
return utils.Location(self.southwest_latitude, self.southwest_longitude)
@property
def total_x(self) -> int:
@ -103,16 +93,17 @@ class City(meta.Base):
def clear_map(self) -> City: # pragma: no cover
"""Create a new `folium.Map` object aligned with the city's viewport.
The map is available via the `.map` property. Note that it is a
mutable objects that is changed from various locations in the code base.
The map is available via the `.map` property. Note that it is mutable
and changed from various locations in the code base.
Returns:
self: enabling method chaining
""" # noqa:DAR203
""" # noqa:DAR203 note:d334120e
self._map = folium.Map(
location=[self.center_latitude, self.center_longitude],
zoom_start=self.initial_zoom,
)
return self
@property # pragma: no cover
@ -221,11 +212,11 @@ class City(meta.Base):
sa.text(
f""" -- # noqa:S608
SELECT DISTINCT
zip_code
{config.CLEAN_SCHEMA}.addresses.zip_code
FROM
{config.CLEAN_SCHEMA}.addresses
{config.CLEAN_SCHEMA}.addresses AS addresses
WHERE
city_id = {self.id};
{config.CLEAN_SCHEMA}.addresses.city_id = {self.id};
""",
),
)

View file

@ -31,7 +31,7 @@ class Forecast(meta.Base):
model = sa.Column(sa.Unicode(length=20), nullable=False)
# We also store the actual order counts for convenient retrieval.
# A `UniqueConstraint` below ensures that redundant values that
# are to be expected are consistend across rows.
# are to be expected are consistent across rows.
actual = sa.Column(sa.SmallInteger, nullable=False)
# Raw `.prediction`s are stored as `float`s (possibly negative).
# The rounding is then done on the fly if required.
@ -157,7 +157,7 @@ class Forecast(meta.Base):
Background: The functions in `urban_meal_delivery.forecasts.methods`
return `pd.Dataframe`s with "start_at" (i.e., `pd.Timestamp` objects)
values in the index and five columns "prediction", "low80", "high80",
"low95", and "high95" with `np.float` values. The `*Model.predic()`
"low95", and "high95" with `np.float` values. The `*Model.predict()`
methods in `urban_meal_delivery.forecasts.models` then add an "actual"
column. This constructor converts these results into ORM models.
Also, the `np.float` values are cast as plain `float` ones as

View file

@ -2,6 +2,7 @@
from __future__ import annotations
import functools
from typing import List
import folium
@ -68,66 +69,58 @@ class Pixel(meta.Base):
"""The area of a pixel in square kilometers."""
return self.grid.pixel_area
@property
@functools.cached_property
def northeast(self) -> utils.Location:
"""The pixel's northeast corner, relative to `.grid.city.southwest`.
Implementation detail: This property is cached as none of the
underlying attributes to calculate the value are to be changed.
"""
if not hasattr(self, '_northeast'): # noqa:WPS421 note:d334120e
# The origin is the southwest corner of the `.grid.city`'s viewport.
easting_origin = self.grid.city.southwest.easting
northing_origin = self.grid.city.southwest.northing
easting, northing = (
self.grid.city.southwest.easting + ((self.n_x + 1) * self.side_length),
self.grid.city.southwest.northing + ((self.n_y + 1) * self.side_length),
)
latitude, longitude = utm.to_latlon(
easting, northing, *self.grid.city.southwest.zone_details,
)
# `+1` as otherwise we get the pixel's `.southwest` corner.
easting = easting_origin + ((self.n_x + 1) * self.side_length)
northing = northing_origin + ((self.n_y + 1) * self.side_length)
zone, band = self.grid.city.southwest.zone_details
latitude, longitude = utm.to_latlon(easting, northing, zone, band)
location = utils.Location(latitude, longitude)
location.relate_to(self.grid.city.southwest)
self._northeast = utils.Location(latitude, longitude)
self._northeast.relate_to(self.grid.city.southwest)
return location
return self._northeast
@property
@functools.cached_property
def southwest(self) -> utils.Location:
"""The pixel's northeast corner, relative to `.grid.city.southwest`.
"""The pixel's southwest corner, relative to `.grid.city.southwest`.
Implementation detail: This property is cached as none of the
underlying attributes to calculate the value are to be changed.
"""
if not hasattr(self, '_southwest'): # noqa:WPS421 note:d334120e
# The origin is the southwest corner of the `.grid.city`'s viewport.
easting_origin = self.grid.city.southwest.easting
northing_origin = self.grid.city.southwest.northing
easting, northing = (
self.grid.city.southwest.easting + (self.n_x * self.side_length),
self.grid.city.southwest.northing + (self.n_y * self.side_length),
)
latitude, longitude = utm.to_latlon(
easting, northing, *self.grid.city.southwest.zone_details,
)
easting = easting_origin + (self.n_x * self.side_length)
northing = northing_origin + (self.n_y * self.side_length)
zone, band = self.grid.city.southwest.zone_details
latitude, longitude = utm.to_latlon(easting, northing, zone, band)
location = utils.Location(latitude, longitude)
location.relate_to(self.grid.city.southwest)
self._southwest = utils.Location(latitude, longitude)
self._southwest.relate_to(self.grid.city.southwest)
return location
return self._southwest
@property
@functools.cached_property
def restaurants(self) -> List[db.Restaurant]: # pragma: no cover
"""Obtain all `Restaurant`s in `self`."""
if not hasattr(self, '_restaurants'): # noqa:WPS421 note:d334120e
self._restaurants = ( # noqa:ECE001
db.session.query(db.Restaurant)
.join(
db.AddressPixelAssociation,
db.Restaurant.address_id == db.AddressPixelAssociation.address_id,
)
.filter(db.AddressPixelAssociation.pixel_id == self.id)
.all()
return ( # noqa:ECE001
db.session.query(db.Restaurant)
.join(
db.AddressPixelAssociation,
db.Restaurant.address_id == db.AddressPixelAssociation.address_id,
)
return self._restaurants
.filter(db.AddressPixelAssociation.pixel_id == self.id)
.all()
)
def clear_map(self) -> Pixel: # pragma: no cover
"""Shortcut to the `.city.clear_map()` method.

View file

@ -15,7 +15,7 @@ class Location: # noqa:WPS214
- assumes earth is a sphere and models the location in 3D
UTM:
- the Universal Transverse Mercator sytem
- the Universal Transverse Mercator system
- projects WGS84 coordinates onto a 2D map
- can be used for visualizations and calculations directly
- distances are in meters
@ -70,7 +70,7 @@ class Location: # noqa:WPS214
@property
def lat_lng(self) -> Tuple[float, float]:
"""The `.latitude` and `.longitude` as a 2-`tuple`."""
return (self._latitude, self._longitude)
return self._latitude, self._longitude
@property
def easting(self) -> int:
@ -90,7 +90,7 @@ class Location: # noqa:WPS214
@property
def zone_details(self) -> Tuple[int, str]:
"""The UTM zone of the location as the zone number and the band."""
return (self._zone, self._band)
return self._zone, self._band
def __eq__(self, other: object) -> bool:
"""Check if two `Location` objects are the same location."""