diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 29d624b..38ac936 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,10 +9,10 @@ jobs: steps: - uses: actions/checkout@v1 - - name: Set up Python 3.7 + - name: Set up Python 3.13 uses: actions/setup-python@v1 with: - python-version: 3.7 + python-version: 3.13 - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/README.md b/README.md index b3fb8e2..ffb151f 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# OsmToRoadGraph v.0.6.0 +# OsmToRoadGraph v.0.7.0 ![Build Status](https://github.com/AndGem/OsmToRoadGraph/workflows/Build%20Status/badge.svg?branch=master) [![codecov](https://codecov.io/gh/AndGem/OsmToRoadGraph/branch/master/graph/badge.svg)](https://codecov.io/gh/AndGem/OsmToRoadGraph) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -- [OsmToRoadGraph v.0.6.0](#osmtoroadgraph-v060) +- [OsmToRoadGraph v.0.7.0](#osmtoroadgraph-v060) - [Updates](#updates) - [Introduction](#introduction) - [Motivation](#motivation) @@ -24,12 +24,12 @@ ## Updates -**Changelog v.0.5.0 -> v.0.6.0c:** -- [x] Added bz2 support for input files -- [x] dropped Python <=3.6 support -- [x] fixed minor issues with v.0.6.0a -- [x] improve code quality from v.0.6.0b +**Changelog v.0.6.0c -> v.0.7.0:** + +- [x] Upgraded to Python 3.13 requirement +- [x] pyproject.toml added +- [x] minor code improvements ## Introduction @@ -53,7 +53,7 @@ As an additional feature, and to make interaction easier, since version 0.5 OsmT ### Requirements -- Python 3.7+/PyPy +- Python 3.13+/PyPy - An OSM XML file - [Optional: [networkx](https://networkx.github.io/) as dependency: `pip3 install networkx`] diff --git a/examples/pycgr-to-png/pycgr-to-png.py b/examples/pycgr-to-png/pycgr-to-png.py index b6e0707..b81f6ee 100644 --- a/examples/pycgr-to-png/pycgr-to-png.py +++ b/examples/pycgr-to-png/pycgr-to-png.py @@ -1,10 +1,10 @@ -from dataclasses import dataclass -from enum import Enum import argparse import sys +from dataclasses import dataclass +from enum import Enum -from PIL import Image, ImageDraw, ImageColor import utm +from PIL import Image, ImageColor, ImageDraw @dataclass diff --git a/examples/shortest-distances-drawing/shortest-distances-drawing.py b/examples/shortest-distances-drawing/shortest-distances-drawing.py index 69877ab..5cadfb2 100644 --- a/examples/shortest-distances-drawing/shortest-distances-drawing.py +++ b/examples/shortest-distances-drawing/shortest-distances-drawing.py @@ -1,21 +1,20 @@ import argparse -from collections import Counter -from functools import lru_cache import json import random import sys +from collections import Counter +from functools import lru_cache import networkx as nx -from PIL import Image, ImageDraw, ImageColor -from tqdm import tqdm import utm +from PIL import Image, ImageColor, ImageDraw +from tqdm import tqdm def load_graph(filename): with open(filename, "r", encoding="utf-8") as f: json_data = json.load(f) - G = nx.adjacency_graph(json_data) - return G + return nx.adjacency_graph(json_data) def find_approximate_central_node(G): @@ -29,9 +28,7 @@ def find_approximate_central_node(G): node_distances += lengths central_node = min(node_distances, key=node_distances.get) - print( - f"taking node with id {central_node} as central node, with summed distance: {node_distances[central_node]}" - ) + print(f"taking node with id {central_node} as central node, with summed distance: {node_distances[central_node]}") return central_node @@ -127,12 +124,8 @@ def edge_length(_u, _v, data): if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument( - "-f", "--file", dest="in_filename", type=str, help="input networkx JSON file" - ) - parser.add_argument( - "-o", "--out", dest="out_filename", type=str, help="output png file" - ) + parser.add_argument("-f", "--file", dest="in_filename", type=str, help="input networkx JSON file") + parser.add_argument("-o", "--out", dest="out_filename", type=str, help="output png file") parser.add_argument( "-c", "--center", @@ -181,11 +174,7 @@ def edge_length(_u, _v, data): elif args.metric == "length": metric = edge_length else: - print( - f"Did not recognize --metric/-m option. Provided {args.metric}. Must either be 'travel-time' or 'length'" - ) + print(f"Did not recognize --metric/-m option. Provided {args.metric}. Must either be 'travel-time' or 'length'") - lengths = nx.single_source_dijkstra_path_length( - G, start_node, cutoff=None, weight=metric - ) + lengths = nx.single_source_dijkstra_path_length(G, start_node, cutoff=None, weight=metric) draw_graph_on_map(G, lengths, output_filename=args.out_filename) diff --git a/graph/algorithms.py b/graph/algorithms.py index bb2e652..ea5fe85 100644 --- a/graph/algorithms.py +++ b/graph/algorithms.py @@ -1,10 +1,9 @@ from collections import deque +from typing import Deque, Set from graph import graphfactory -from utils import timer - from graph.graph import Graph -from typing import Deque, Set +from utils import timer def BFS(graph: Graph, s: int) -> Set[int]: @@ -13,11 +12,7 @@ def BFS(graph: Graph, s: int) -> Set[int]: while unvisited_nodes: current_node = unvisited_nodes.popleft() - unseen_nodes = [ - neighbor - for neighbor in graph.all_neighbors(current_node) - if neighbor not in seen_nodes - ] + unseen_nodes = [neighbor for neighbor in graph.all_neighbors(current_node) if neighbor not in seen_nodes] seen_nodes.update(unseen_nodes) unvisited_nodes.extend(unseen_nodes) @@ -40,14 +35,12 @@ def computeLCC(graph): # determine largest connected components lcc = max(found_nodes, key=len) - print( - f"\t LCC contains {len(lcc)} nodes (removed { len(graph.vertices) - len(lcc)} nodes)" - ) + print(f"\t LCC contains {len(lcc)} nodes (removed { len(graph.vertices) - len(lcc)} nodes)") return lcc def computeLCCGraph(graph: Graph) -> Graph: lcc = computeLCC(graph) - new_nodes = [graph.vertices[id] for id in lcc] + new_nodes = [graph.vertices[vertex_id] for vertex_id in lcc] return graphfactory.build_graph_from_vertices_edges(new_nodes, graph.edges) diff --git a/graph/contract_graph.py b/graph/contract_graph.py index f7241fb..52350e3 100644 --- a/graph/contract_graph.py +++ b/graph/contract_graph.py @@ -1,12 +1,11 @@ -from collections import deque, defaultdict +from collections import defaultdict, deque from dataclasses import replace from typing import DefaultDict, List, Set, Tuple from graph import graphfactory -from utils import timer - from graph.graph import Graph -from graph.graph_types import Vertex, Edge +from graph.graph_types import Edge, Vertex +from utils import timer class ContractGraph: @@ -47,9 +46,7 @@ def _find_new_edges(self) -> Set[Edge]: for first_out_edge in out_edges: start_node_id = node_id - edges_to_merge, final_node_id = self._find_edges_to_merge( - start_node_id, first_out_edge - ) + edges_to_merge, final_node_id = self._find_edges_to_merge(start_node_id, first_out_edge) if len(edges_to_merge) == 0: continue @@ -60,16 +57,8 @@ def _find_new_edges(self) -> Set[Edge]: if edges_to_merge[0].backward: # deduplication measure; if not for this for bidirectional edges, that are # removed between intersections, 2 new edges would be created - smaller_node_id = ( - start_node_id - if start_node_id < final_node_id - else final_node_id - ) - bigger_node_id = ( - start_node_id - if start_node_id > final_node_id - else final_node_id - ) + smaller_node_id = start_node_id if start_node_id < final_node_id else final_node_id + bigger_node_id = start_node_id if start_node_id > final_node_id else final_node_id if (smaller_node_id, bigger_node_id) in bidirectional_edges: # already added this edge skip it continue @@ -92,9 +81,7 @@ def _find_new_edges(self) -> Set[Edge]: new_edges.add(merged_edge) return new_edges - def _find_edges_to_merge( - self, start_node_id: int, first_out_edge: Edge - ) -> Tuple[List[Edge], int]: + def _find_edges_to_merge(self, start_node_id: int, first_out_edge: Edge) -> Tuple[List[Edge], int]: # walk from start_node along first_out_edge until: # i) another intersection node is found # ii) an edge is encountered on the way that is different (different name, max_speed, ...) @@ -127,7 +114,7 @@ def _find_edges_to_merge( if len(next_out_edges) > 1: # something is wrong.. this should have been filtered out by the intersection check - assert False + raise AssertionError() next_out_edge = next_out_edges[0] if self._is_not_same_edge(out_edge, next_out_edge): diff --git a/graph/convert_graph.py b/graph/convert_graph.py index 2b94300..2699ed4 100644 --- a/graph/convert_graph.py +++ b/graph/convert_graph.py @@ -1,7 +1,7 @@ -try: +import contextlib + +with contextlib.suppress(ImportError): import networkx as nx -except ImportError: - pass def convert_to_networkx(graph): diff --git a/graph/graph.py b/graph/graph.py index f0a3651..9b62a1c 100644 --- a/graph/graph.py +++ b/graph/graph.py @@ -1,6 +1,7 @@ -from graph.graph_types import Edge, Vertex from typing import List +from graph.graph_types import Edge, Vertex + class Graph: def __init__(self) -> None: @@ -35,6 +36,4 @@ def edge_name(self, edge_id): return f"{self.edges[edge_id].name}" def all_neighbors(self, node_id: int) -> List[int]: - return list( - set(self.outneighbors[node_id]).union(set(self.inneighbors[node_id])) - ) + return list(set(self.outneighbors[node_id]).union(set(self.inneighbors[node_id]))) diff --git a/graph/graphfactory.py b/graph/graphfactory.py index 2f41e37..b7e2a59 100644 --- a/graph/graphfactory.py +++ b/graph/graphfactory.py @@ -1,12 +1,11 @@ from dataclasses import replace +from typing import Dict, List, Tuple from graph.graph import Graph -from graph.graph_types import Vertex, Edge, VertexData, EdgeData +from graph.graph_types import Edge, EdgeData, Vertex, VertexData from osm.osm_types import OSMNode, OSMWay from utils import geo_tools, timer -from typing import Dict, List, Tuple - @timer.timer def build_graph_from_osm(nodes: Dict[int, OSMNode], ways: List[OSMWay]) -> Graph: @@ -14,7 +13,7 @@ def build_graph_from_osm(nodes: Dict[int, OSMNode], ways: List[OSMWay]) -> Graph # 1. create mapping to 0 based index nodes node_ids = nodes.keys() - id_mapper = dict(zip(node_ids, range(len(node_ids)))) + id_mapper = dict(zip(node_ids, range(len(node_ids)), strict=True)) # 2. add nodes and edges _add_nodes(g, id_mapper, nodes) @@ -34,20 +33,14 @@ def _add_edges(g: Graph, id_mapper: Dict[int, int], ways: List[OSMWay]) -> None: for i in range(len(w.nodes) - 1): s_id, t_id = id_mapper[w.nodes[i]], id_mapper[w.nodes[i + 1]] s, t = g.vertices[s_id], g.vertices[t_id] - length = round( - geo_tools.distance(s.data.lat, s.data.lon, t.data.lat, t.data.lon), 2 - ) - data = EdgeData( - length=length, highway=w.highway, max_v=w.max_speed_int, name=w.name - ) + length = round(geo_tools.distance(s.data.lat, s.data.lon, t.data.lat, t.data.lon), 2) + data = EdgeData(length=length, highway=w.highway, max_v=w.max_speed_int, name=w.name) edge = Edge(s_id, t_id, w.forward, w.backward, data=data) if w.forward and w.backward: smaller, bigger = min(s_id, t_id), max(s_id, t_id) if (smaller, bigger) in bidirectional_edges: print(f"found duplicated bidirectional edge {(smaller, bigger)}") - print( - f"(osm ids {w.osm_id} and {bidirectional_edges[(smaller, bigger)]})... skipping one" - ) + print(f"(osm ids {w.osm_id} and {bidirectional_edges[(smaller, bigger)]})... skipping one") continue bidirectional_edges[(smaller, bigger)] = w.osm_id @@ -59,8 +52,8 @@ def build_graph_from_vertices_edges(vertices: List[Vertex], edges: List[Edge]) - g = Graph() # 1. add all nodes and create mapping to 0 based index nodes - vertex_ids = set(v.id for v in vertices) - id_mapper = dict(zip(vertex_ids, range(len(vertex_ids)))) + vertex_ids = {v.id for v in vertices} + id_mapper = dict(zip(vertex_ids, range(len(vertex_ids)), strict=True)) for v in vertices: g.add_node(Vertex(id_mapper[v.id], v.data)) diff --git a/osm/read_osm.py b/osm/read_osm.py index 102fdec..b93657f 100644 --- a/osm/read_osm.py +++ b/osm/read_osm.py @@ -1,13 +1,12 @@ import bz2 import xml.sax +from typing import Dict, List, Set, Tuple from osm.osm_types import OSMNode, OSMWay from osm.way_parser_helper import WayParserHelper -from osm.xml_handler import NodeHandler, WayHandler, PercentageFile +from osm.xml_handler import NodeHandler, PercentageFile, WayHandler from utils import timer -from typing import Dict, List, Set, Tuple - @timer.timer def read_file(osm_filename, configuration) -> Tuple[Dict[int, OSMNode], List[OSMWay]]: @@ -31,10 +30,10 @@ def decompress_content(osm_filename): if content_begin.startswith(magic_bz2): print("identified bz2 compressed file.. decompressing") - f = bz2.open(osm_filename, "rb") - content = f.read() - print("done!") - return content + with bz2.open(osm_filename, "rb") as f: + content = f.read() + print("done!") + return content print("no compression recognized!") return None diff --git a/osm/sanitize_input.py b/osm/sanitize_input.py index e88b8cd..a4b7061 100644 --- a/osm/sanitize_input.py +++ b/osm/sanitize_input.py @@ -43,9 +43,7 @@ def remove_adjacent_duplicates(nodes): del ways[index] # determine nodes that do not appear in any of the ways - nodes_to_remove = [ - osm_id for osm_id in nodes.keys() if osm_id not in found_node_ids - ] + nodes_to_remove = [osm_id for osm_id in nodes if osm_id not in found_node_ids] # remove these nodes for index in reversed(nodes_to_remove): del nodes[index] diff --git a/osm/way_parser_helper.py b/osm/way_parser_helper.py index acae0a7..c0096b9 100644 --- a/osm/way_parser_helper.py +++ b/osm/way_parser_helper.py @@ -46,8 +46,7 @@ def parse_max_speed(self, osm_way: OSMWay) -> int: return self.config.speed_limits[highway] try: - max_speed = int(maximum_speed) - return max_speed + return int(maximum_speed) except ValueError: max_speed_str = maximum_speed.lower() @@ -76,8 +75,6 @@ def parse_max_speed(self, osm_way: OSMWay) -> int: max_speed = self.config.speed_limits[highway] else: max_speed = 30 - print( - f"couldn't find a speed limit for highway type {highway}! Setting it to {max_speed}" - ) + print(f"couldn't find a speed limit for highway type {highway}! Setting it to {max_speed}") return max_speed diff --git a/osm/xml_handler.py b/osm/xml_handler.py index a3cf0ca..a7b7f66 100644 --- a/osm/xml_handler.py +++ b/osm/xml_handler.py @@ -1,10 +1,10 @@ import os import sys -from typing import Optional, Set, List, Dict -from xml.sax.xmlreader import AttributesImpl +from typing import Dict, List, Optional, Set from xml.sax.handler import ContentHandler +from xml.sax.xmlreader import AttributesImpl -from osm.osm_types import OSMWay, OSMNode +from osm.osm_types import OSMNode, OSMWay from osm.way_parser_helper import WayParserHelper intern = sys.intern @@ -52,9 +52,7 @@ def startElement(self, name: str, attrs: AttributesImpl) -> None: if osm_id not in self.found_nodes: return - self.nodes[osm_id] = OSMNode( - osm_id, float(attrs["lat"]), float(attrs["lon"]) - ) + self.nodes[osm_id] = OSMNode(osm_id, float(attrs["lat"]), float(attrs["lon"])) class WayHandler(ContentHandler): @@ -115,9 +113,7 @@ def endElement(self, name: str) -> None: self.found_nodes.update(self.current_way.nodes) - self.current_way.max_speed_int = self.parser_helper.parse_max_speed( - self.current_way - ) + self.current_way.max_speed_int = self.parser_helper.parse_max_speed(self.current_way) ( self.current_way.forward, self.current_way.backward, diff --git a/output/write_graph.py b/output/write_graph.py index 6d2a270..a671ad3 100644 --- a/output/write_graph.py +++ b/output/write_graph.py @@ -1,10 +1,9 @@ import codecs +import contextlib import json -try: +with contextlib.suppress(ImportError): import networkx as nx -except ImportError: - pass def write_to_file(graph, filename_base, filename_ext): @@ -19,9 +18,7 @@ def write_to_file(graph, filename_base, filename_ext): # edge_properties # ...""" - with open(filename, "w", encoding="utf-8") as f, codecs.open( - f"{filename}_names", "w", "utf-8" - ) as f_names: + with open(filename, "w", encoding="utf-8") as f, codecs.open(f"{filename}_names", "w", "utf-8") as f_names: f.write(f"{file_header}\n") f.write(f"{header}\n") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..537a562 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,23 @@ +[project] +name = "OsmToRoadGraph" +version = "0.7.0" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [] + +[project.optional-dependencies] +dev = ["pytest"] +[tool.black] +line-length = 120 +target-version = ['py311'] + +[tool.ruff.lint] +select = ["I", "F", "SIM", "E", "A", "ARG", "B", "C4", "SIM", "TC", "RET", "TID", "Q"] +ignore = ["E501"] +fixable = ["ALL"] + +[dependency-groups] +dev = [ + "black>=25.1.0", + "ruff>=0.12.5", +] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index e69de29..0000000 diff --git a/run.py b/run.py index 0c9007e..e828f07 100644 --- a/run.py +++ b/run.py @@ -4,9 +4,9 @@ import sys import configuration as config +from graph import algorithms, contract_graph, convert_graph, graphfactory from osm import read_osm, sanitize_input from output import write_graph as output -from graph import contract_graph, convert_graph, algorithms, graphfactory from utils import timer @@ -42,9 +42,7 @@ def convert_osm_to_roadgraph(filename, network_type, options): if options.contract: contracted_graph = contract_graph.ContractGraph(graph).contract() - output.write_to_file( - contracted_graph, out_file, f"{configuration.get_file_extension()}c" - ) + output.write_to_file(contracted_graph, out_file, f"{configuration.get_file_extension()}c") if options.networkx_output: nx_graph = convert_graph.convert_to_networkx(contracted_graph) output.write_nx_to_file(nx_graph, f"{out_file}_contracted.json") @@ -90,9 +88,7 @@ def validate_networkx(): try: if not os.path.isfile(filename): - raise FileNotFoundError( - f"Provided filename {filename} does not point to a file!" - ) + raise FileNotFoundError(f"Provided filename {filename} does not point to a file!") long_network_type = {"p": "pedestrian", "c": "car", "b": "bicycle"} if options.network_type in long_network_type: diff --git a/tests/contract_graph_test.py b/tests/contract_graph_test.py index 553c2d2..8cca629 100644 --- a/tests/contract_graph_test.py +++ b/tests/contract_graph_test.py @@ -1,8 +1,8 @@ import unittest -from graph.graph import Graph -from graph.graph_types import Edge, Vertex, EdgeData, VertexData from graph.contract_graph import ContractGraph +from graph.graph import Graph +from graph.graph_types import Edge, EdgeData, Vertex, VertexData class ContractGraphTest(unittest.TestCase): @@ -161,9 +161,7 @@ def test_contracting_stops_at_intersections(self): def test_no_contraction_if_each_edge_is_different(self): g = self.create_graph(number_nodes=6) - g.add_edge( - Edge(s=0, t=1, forward=True, backward=True, data=self.edge_data(name="a")) - ) + g.add_edge(Edge(s=0, t=1, forward=True, backward=True, data=self.edge_data(name="a"))) g.add_edge( Edge( s=1, diff --git a/tests/convert_graph_test.py b/tests/convert_graph_test.py index 38e3228..d12c88c 100644 --- a/tests/convert_graph_test.py +++ b/tests/convert_graph_test.py @@ -2,17 +2,17 @@ import string import unittest -from graph.graph import Graph -from graph.graph_types import Edge, Vertex, EdgeData, VertexData from graph.convert_graph import convert_to_networkx +from graph.graph import Graph +from graph.graph_types import Edge, EdgeData, Vertex, VertexData def random_string(length=8): return "".join(random.choice(string.ascii_lowercase) for i in range(length)) -def random_uint(max=1000): - return random.randint(1, max) +def random_uint(max_val=1000): + return random.randint(1, max_val) class TestConvertGraphTest(unittest.TestCase): diff --git a/tests/graph_test.py b/tests/graph_test.py index fe70149..cbbd2e1 100644 --- a/tests/graph_test.py +++ b/tests/graph_test.py @@ -1,7 +1,7 @@ import unittest from graph.graph import Graph -from graph.graph_types import Edge, Vertex, EdgeData, VertexData +from graph.graph_types import Edge, EdgeData, Vertex, VertexData class GraphTest(unittest.TestCase): diff --git a/tests/graphfactory_test.py b/tests/graphfactory_test.py index 4448820..fc455e5 100644 --- a/tests/graphfactory_test.py +++ b/tests/graphfactory_test.py @@ -1,7 +1,7 @@ import unittest import graph.graphfactory as gf -from graph.graph_types import Vertex, Edge, VertexData, EdgeData +from graph.graph_types import Edge, EdgeData, Vertex, VertexData class GraphfactorTest(unittest.TestCase): diff --git a/tests/integration_test.py b/tests/integration_test.py index 91773c2..36152e5 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -36,12 +36,12 @@ def _execute_program(self): self.assertEqual(0, returncode) def _get_nmb_nodes_edges(self, path): - f = open(path) - line = f.readline() - while "#" in line: + with open(path) as f: line = f.readline() + while "#" in line: + line = f.readline() - nmb_nodes = int(line) - line = f.readline() - nmb_edges = int(line) - return nmb_nodes, nmb_edges + nmb_nodes = int(line) + line = f.readline() + nmb_edges = int(line) + return nmb_nodes, nmb_edges diff --git a/tests/utils_test.py b/tests/utils_test.py index e51e355..df06f10 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -1,7 +1,9 @@ # Automatically generated by Pynguin. +import unittest + import pytest + import utils.geo_tools as module0 -import unittest class IntegrationTest(unittest.TestCase): diff --git a/utils/timer.py b/utils/timer.py index d0b72cd..b22559c 100644 --- a/utils/timer.py +++ b/utils/timer.py @@ -1,5 +1,4 @@ import time - from typing import Callable