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

chore: Introduce ruff #10

Merged
merged 2 commits into from
Feb 1, 2025
Merged
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
43 changes: 8 additions & 35 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v5.0.0
hooks:
# Identify invalid files
- id: check-ast
Expand All @@ -27,42 +27,15 @@ repos:
args: [--markdown-linebreak-ext=md]
- id: end-of-file-fixer

- repo: https://github.com/PyCQA/autoflake
rev: v2.3.1
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.4
hooks:
- id: autoflake

- repo: https://github.com/asottile/pyupgrade
rev: v3.15.2
hooks:
- id: pyupgrade
args:
- '--py39-plus'

- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
name: isort
args:
- '.'

- repo: https://github.com/psf/black
rev: 24.4.2
hooks:
- id: black

- repo: https://github.com/pycqa/flake8
rev: 7.0.0
hooks:
- id: flake8
additional_dependencies:
- 'Flake8-pyproject~=1.1'
args:
- '.'
- id: ruff
args: [ --fix ]
- id: ruff-format

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.3.0
rev: v1.14.1
hooks:
- id: mypy
args:
Expand All @@ -74,7 +47,7 @@ repos:
exclude: ^tests/

- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.40.0
rev: v0.44.0
hooks:
- id: markdownlint
args:
Expand Down
13 changes: 9 additions & 4 deletions pydantic_glue/cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""CLI to convert pydantic class to AWS Glue schema."""

import json
import logging
import os
import sys
from argparse import ArgumentParser
from dataclasses import dataclass
Expand All @@ -15,6 +16,8 @@

@dataclass
class Arguments:
"""CLI Arguments."""

module_file: str
class_name: str
output_file: str
Expand All @@ -23,6 +26,7 @@ class Arguments:


def parse_args(argv: list[str]) -> Arguments:
"""Parse CLI arguments."""
parser = ArgumentParser()
parser.add_argument("-f", dest="source_file", required=True, type=str, help="Path to the python file")
parser.add_argument("-c", dest="class_name", required=True, type=str, help="Python class name")
Expand All @@ -49,9 +53,10 @@ def parse_args(argv: list[str]) -> Arguments:


def cli() -> None:
"""CLI entry point."""
args = parse_args(sys.argv[1:])
sys.path.append(os.path.dirname(args.module_file))
imported = __import__(os.path.basename(args.module_file))
sys.path.append(Path(args.module_file).parent.as_posix())
imported = __import__(Path(args.module_file).name)

model = getattr(imported, args.class_name)
input_schema = json.dumps(model.model_json_schema(by_alias=args.json_schema_by_alias))
Expand All @@ -60,7 +65,7 @@ def cli() -> None:
output = json.dumps(
{
"//": f"Generated by pydantic-glue at {datetime.now(tz=timezone.utc)}. DO NOT EDIT",
"columns": {k: v for (k, v) in converted},
"columns": dict(converted),
},
indent=2,
)
Expand Down
16 changes: 16 additions & 0 deletions pydantic_glue/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Errors."""


class ObjectWithoutPropertiesError(Exception):
def __init__(self) -> None:
super().__init__("Object without properties or additionalProperties can't be represented")


class UnknownTypeError(Exception):
def __init__(self, type_name: str) -> None:
super().__init__(f"Unknown type: {type_name}")


class GlueMapWithoutTypesError(Exception):
def __init__(self) -> None:
super().__init__("Glue Cannot Support a Map Without Types")
50 changes: 28 additions & 22 deletions pydantic_glue/handler.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
"""Convert Json schema to glue."""

from __future__ import annotations

from typing import Any, Union

import jsonref

from pydantic_glue.errors import GlueMapWithoutTypesError, ObjectWithoutPropertiesError, UnknownTypeError

def dispatch(v: dict[str, Any]) -> str:

glue_type = v.get("glue_type", None)

if glue_type is not None:
def dispatch(v: dict[str, Any]) -> str: # noqa: PLR0911
"""Dispatch json schema element as glue type."""
if glue_type := v.get("glue_type"):
return str(glue_type)

if "anyOf" in v:
return handle_union(v)
return _handle_union(v)

t = v["type"]

if t == "object":
return handle_object(v)
return _handle_object(v)

if t == "array":
return handle_array(v)
return _handle_array(v)

if t == "string":
if v.get("format") == "date-time":
Expand All @@ -37,53 +41,55 @@ def dispatch(v: dict[str, Any]) -> str:
if t == "number":
return "float"

raise Exception(f"unknown type: {t}")
raise UnknownTypeError(t)


def handle_map(o: dict[str, Any]) -> str:
def _handle_map(o: dict[str, Any]) -> str:
t = o["additionalProperties"]
res = dispatch(t)
return f"map<string,{res}>"


def handle_union(o: dict[str, Any]) -> str:
def _handle_union(o: dict[str, Any]) -> str:
types = [i for i in o["anyOf"] if i["type"] != "null"]
if len(types) > 1:
res = [dispatch(v) for v in types]
return f"union<{','.join(res)}>"
return dispatch(types[0])


def map_dispatch(o: dict[str, Any]) -> list[tuple[str, str]]:
def _map_dispatch(o: dict[str, Any]) -> list[tuple[str, str]]:
return [(k, dispatch(v)) for k, v in o["properties"].items()]


def handle_object(o: dict[str, Any]) -> str:
def _handle_object(o: dict[str, Any]) -> str:
if "additionalProperties" in o:
if o["additionalProperties"] is True:
raise Exception("Glue Cannot Support a Map Without Types")
elif o["additionalProperties"]:
raise GlueMapWithoutTypesError
if o["additionalProperties"]:
if "properties" in o:
raise NotImplementedError("Merging types of properties and additionalProperties")
return handle_map(o)
msg = "Merging types of properties and additionalProperties"
raise NotImplementedError(msg)
return _handle_map(o)

if "properties" not in o:
raise Exception("Object without properties or additionalProperties can't be represented")
raise ObjectWithoutPropertiesError

res = [f"{k}:{v}" for (k, v) in map_dispatch(o)]
res = [f"{k}:{v}" for (k, v) in _map_dispatch(o)]
return f"struct<{','.join(res)}>"


def handle_array(o: dict[str, Any]) -> str:
def _handle_array(o: dict[str, Any]) -> str:
t = dispatch(o["items"])
return f"array<{t}>"


def handle_root(o: dict[str, Any]) -> list[tuple[str, str]]:
return map_dispatch(o)
def _handle_root(o: dict[str, Any]) -> list[tuple[str, str]]:
return _map_dispatch(o)


def convert(schema: str) -> Union[list[Any], list[tuple[str, str]]]:
"""Convert json schema to glue."""
if not schema:
return []
return handle_root(jsonref.loads(schema))
return _handle_root(jsonref.loads(schema))
82 changes: 54 additions & 28 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,46 +21,72 @@ jsonref = "^1.1.0"
pydantic = "^2.7.1"

[tool.poetry.group.dev.dependencies]
autoflake = "^2.3.1"
pytest = "^8.2.0"
cli-test-helpers = "^4.1.0"
mypy = "^1.10.0"
flake8 = "^7.0.0"
black = "^24.4.2"
isort = "^5.13.2"
pre-commit = "^3.7.0"
cli-test-helpers = "^4.1.0"

[tool.autoflake]
recursive = true
in-place = true
remove-all-unused-imports = true
remove-unused-variables = true
ruff = "^0.9.4"
pytest-cov = "^6.0.0"

[tool.black]
[tool.ruff]
line-length = 120
target-version = ['py39', 'py310', 'py311', 'py312']
include = '\.pyi?$'
indent-width = 4
target-version = "py39"

[tool.flake8]
files = '.*\.py'
max-line-length = 120
exclude = ['.git', '.eggs', '__pycache__', 'venv', '.venv']
[tool.ruff.lint]
select = ["ALL"]
ignore = [
# space before: (needed for how black formats slicing)
'E203',
# line break before binary operator (needed for how black formats long lines)
'W503'
# The following rules may cause conflicts when used with the formatter
"COM812",
"ISC001",

# General ignores
"ANN204", # Missing return type annotation for special method `__init__`
"D107", # Missing docstring in `__init__`
"D203", # one-blank-line-before-class
"D213", # multi-line-summary-second-line
"G004", # Logging statement uses f-string
"UP007", # Use `X | Y` for type annotations
]

[tool.isort]
profile = 'black'
src_paths = ['athena_udf', 'test']
fixable = ["ALL"]
unfixable = []
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"

[tool.pytest.ini_options]
testpaths = [
"tests"
[tool.ruff.lint.per-file-ignores]
"**/{tests}/*" = [
"ANN001", # Missing type annotation for function argument
"ANN201", # Missing return type annotation for public function
"D100", # Missing docstring in public module
"D101", # Missing docstring in public class
"D103", # Missing docstring in public function
"S101", # Use of `assert` detected
"S603", # `subprocess` call: check for execution of untrusted input
]
"__init__.py" = ["D104"] # Missing docstring in public package

"pydantic_glue/errors.py" = [
"D101", # Missing docstring in public class
]

[tool.ruff.lint.flake8-type-checking]
runtime-evaluated-base-classes = ["pydantic.BaseModel"]

[tool.ruff.lint.mccabe]
max-complexity = 15

[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"
docstring-code-format = false
docstring-code-line-length = "dynamic"

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "--cov"

[tool.mypy]
ignore_missing_imports = true
strict = true
Expand Down
Empty file added tests/data/__init__.py
Empty file.
Empty file added tests/unit/__init__.py
Empty file.
8 changes: 3 additions & 5 deletions tests/unit/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import json
from pathlib import Path
from unittest import TestCase

from cli_test_helpers import shell

from pydantic_glue.cli import parse_args


def test_parse_args():

args = parse_args(["-f", "foo/bar/file.py", "-c", "Test"])
assert args.module_file == "foo/bar/file"
assert args.class_name == "Test"
Expand All @@ -17,7 +16,6 @@ def test_parse_args():


def test_parse_args_schema_by_name():

args = parse_args(["-f", "foo/bar/file.py", "-c", "Test", "--schema-by-name"])
assert args.module_file == "foo/bar/file"
assert args.class_name == "Test"
Expand All @@ -30,11 +28,11 @@ def test_cli():
shell("pydantic-glue -f tests/data/input.py -c A -o tests/tmp/actual.json")
actual = json.loads(Path("tests/tmp/actual.json").read_text())
expected = json.loads(Path("tests/data/expected.json").read_text())
TestCase().assertDictEqual(actual["columns"], expected["columns"])
assert actual["columns"] == expected["columns"]


def test_cli_schema_by_name():
shell("pydantic-glue -f tests/data/input.py -c A -o tests/tmp/actual_by_name.json --schema-by-name")
actual = json.loads(Path("tests/tmp/actual_by_name.json").read_text())
expected = json.loads(Path("tests/data/expected_by_name.json").read_text())
TestCase().assertDictEqual(actual["columns"], expected["columns"])
assert actual["columns"] == expected["columns"]
Loading