Skip to content

Commit 65fe5ab

Browse files
Merge pull request #105 from DavidGeorge528/uv-migration
UV Migration (UV, Ruff, Pandas v2)
2 parents 0fd6667 + 8639392 commit 65fe5ab

File tree

13 files changed

+822
-845
lines changed

13 files changed

+822
-845
lines changed

.github/workflows/tests.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,23 @@ jobs:
1010
strategy:
1111
matrix:
1212
os: [ubuntu-latest, macos-latest, windows-latest]
13-
python-version: ['3.8', '3.9', '3.10', '3.11']
13+
python-version: ['3.9', '3.10', '3.11', '3.12']
1414

1515
steps:
1616
- uses: actions/checkout@v3
1717
- name: Set up Python ${{ matrix.python-version }}
1818
uses: actions/setup-python@v3
1919
with:
2020
python-version: ${{ matrix.python-version }}
21+
# see https://github.com/pre-commit/action/#using-this-action
22+
- name: pre-commit checks
23+
uses: pre-commit/action@v3.0.1
24+
env:
25+
# it's okay for github to commit to main/master
26+
SKIP: no-commit-to-branch
2127
- name: Install dependencies
2228
run: |
2329
python -m pip install --upgrade pip
24-
pip install tox tox-gh-actions poetry==1.4.1
30+
pip install tox tox-gh-actions uv tox-uv
2531
- name: Test with tox
2632
run: tox

.pre-commit-config.yaml

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
repos:
2-
- repo: https://github.com/psf/black
3-
rev: 23.1.0
2+
- repo: https://github.com/astral-sh/ruff-pre-commit
3+
rev: v0.8.6
44
hooks:
5-
- id: black
6-
- repo: https://github.com/pycqa/isort
7-
rev: 5.12.0
8-
hooks:
9-
- id: isort
10-
- repo: https://github.com/charliermarsh/ruff-pre-commit
11-
rev: v0.0.254
12-
hooks:
13-
- id: ruff
5+
- id: ruff
6+
args: [ --fix, --exit-non-zero-on-fix ]
7+
- id: ruff-format

makefile

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
PKG_NAME = quiffen
22

3-
.PHONY: test
3+
.PHONY: test
44
test:
5-
( cd tests ; poetry run pytest )
5+
( cd tests ; uv run pytest )
66

7-
.PHONY: lint
7+
.PHONY: lint
88
lint:
9-
( poetry run pylint $(PKG_NAME); poetry run pylint tests )
9+
( uv run ruff check $(PKG_NAME); uv run ruff check tests )
1010

11-
.PHONY: wheel
11+
.PHONY: wheel
1212
wheel:
13-
poetry build -f wheel
13+
uv build --wheel
1414

15-
.PHONY: install
15+
.PHONY: install
1616
install: wheel
17-
poetry install
17+
uv sync
1818

19-
.PHONY: reinstall
19+
.PHONY: reinstall
2020
reinstall: clean install
2121

22-
.PHONY: clean
22+
.PHONY: clean
2323
clean:
24-
find . -type d -name __pycache__ -prune -exec rm -r {} \;
25-
rm -fr build dist *.egg-info test2.qif
24+
find . -type d -name __pycache__ -prune -exec rm -r {} \;
25+
rm -fr build dist *.egg-info test2.qif

poetry.lock

Lines changed: 0 additions & 712 deletions
This file was deleted.

pyproject.toml

Lines changed: 71 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,93 @@
1-
[tool.poetry]
1+
[project]
2+
authors = [
3+
{name = "Isaac Harris-Holt", email = "isaac@harris-holt.com"},
4+
]
5+
license = {text = "GPL-3.0-or-later"}
6+
requires-python = ">=3.9,<3.13"
7+
dependencies = [
8+
"pydantic>=1.10.2,<2.11.0",
9+
"python-dateutil>=2.8.2,<3.0.0",
10+
]
211
name = "quiffen"
312
version = "2.0.13"
413
description = "Quiffen"
5-
authors = ["Isaac Harris-Holt <isaac@harris-holt.com>"]
6-
license = "GPL-3.0-or-later"
714
readme = "README.rst"
8-
homepage = "https://github.com/isaacharrisholt/quiffen"
9-
repository = "https://github.com/isaacharrisholt/quiffen"
10-
documentation = "https://quiffen.readthedocs.io/en/latest/"
11-
keywords = ['qif', 'finance', 'data processing']
15+
keywords = [
16+
"qif",
17+
"finance",
18+
"data processing",
19+
]
1220
classifiers = [
13-
"Development Status :: 5 - Production/Stable",
14-
"Intended Audience :: Developers",
15-
"Programming Language :: Python :: 3",
16-
"Operating System :: MacOS :: MacOS X",
17-
"Operating System :: Microsoft :: Windows",
21+
"Development Status :: 5 - Production/Stable",
22+
"Intended Audience :: Developers",
23+
"Programming Language :: Python :: 3",
24+
"Operating System :: MacOS :: MacOS X",
25+
"Operating System :: Microsoft :: Windows",
1826
]
1927

28+
[project.urls]
29+
homepage = "https://github.com/isaacharrisholt/quiffen"
30+
repository = "https://github.com/isaacharrisholt/quiffen"
31+
documentation = "https://quiffen.readthedocs.io/en/latest/"
2032

21-
[tool.poetry.dependencies]
22-
python = "^3.8"
23-
pydantic = "^1.10.2"
24-
pandas = "^1.5.1"
25-
python-dateutil = "^2.8.2"
26-
33+
[project.optional-dependencies]
34+
pandas = [
35+
"pandas>2.2.0,<3.0.0",
36+
]
2737

28-
[tool.poetry.group.dev.dependencies]
29-
pytest = "^7.2.0"
30-
black = "^23.1.0"
31-
isort = "^5.12.0"
32-
ruff = "^0.0.254"
33-
tox = "^4.4.6"
34-
pyright = "^1.1.296"
35-
pre-commit = "^3.1.1"
38+
[dependency-groups]
39+
dev = [
40+
"pytest<8.0.0,>=7.2.0",
41+
"ruff>=0.0.254,<1.0.0",
42+
"tox<5.0.0,>=4.4.6",
43+
"pyright<2.0.0,>=1.1.296",
44+
"pre-commit<4.0.0,>=3.1.1",
45+
"tox-uv>=1.17.0",
46+
"pandas-stubs>=2.2.2.240807",
47+
"mypy>=1.14.1",
48+
"types-python-dateutil>=2.9.0.20241206",
49+
]
3650

51+
[tool.pdm.build]
52+
includes = []
3753

3854
[build-system]
39-
requires = ["poetry-core"]
40-
build-backend = "poetry.core.masonry.api"
41-
42-
[tool.isort]
43-
profile = "black"
44-
include_trailing_comma = true
55+
requires = ["pdm-backend"]
56+
build-backend = "pdm.backend"
4557

4658
[tool.ruff]
4759
line-length = 88
60+
target-version = "py39"
61+
62+
[tool.ruff.lint]
63+
# see https://docs.astral.sh/ruff/configuration/#using-pyprojecttoml
64+
select = [
65+
# default Ruff checkers: E4, E7, E9, F
66+
"E4",
67+
"E7",
68+
"E9",
69+
# "F" contains autoflake, see https://github.com/astral-sh/ruff/issues/1647
70+
"F", # pyflakes
4871

49-
[tool.ruff.per-file-ignores]
72+
"I", # isort
73+
74+
# TODO: add more rules. For example:
75+
# "B", # flake8-bugbear
76+
# "C4", # flake8-comprehensions
77+
# "EXE", # flake8-executable
78+
# "ICN", # flake8-import-conventions
79+
# "ISC", # flake8-implicit-str-concat
80+
# "PL", # pylint
81+
# "RUF",
82+
# "UP", # pyupgrade
83+
]
84+
85+
[tool.ruff.lint.per-file-ignores]
5086
"__init__.py" = ["F401"] # Ignore unused imports in __init__.py
5187

5288
[tool.pyright]
53-
pythonVersion = "3.8"
89+
pythonVersion = "3.9"
5490

5591
[tool.pytest.ini_options]
5692
testpaths = ["tests"]
93+

quiffen/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
and also to create a QIF structure and then output to either a QIF file, a CSV
66
of transaction data or a pandas DataFrame.
77
"""
8+
89
from quiffen.core.account import Account, AccountType
910
from quiffen.core.base import Field
1011
from quiffen.core.category import (

quiffen/core/category.py

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from decimal import Decimal
44
from enum import Enum
5-
from typing import Any, Dict, Iterable, List, Optional, Union
5+
from typing import Any, Dict, Iterable, List, Optional, TypeVar, Union
66

77
from pydantic import validator
88

@@ -125,9 +125,14 @@ def __str__(self) -> str:
125125
)
126126
return "Category:" + properties
127127

128-
def __lt__(self, other) -> bool:
128+
def __lt__(self, other: Category) -> bool:
129129
return self.name < other.name
130130

131+
def __eq__(self, other: object) -> bool:
132+
if not isinstance(other, Category):
133+
return False
134+
return self.dict() == other.dict()
135+
131136
@validator("hierarchy", pre=True, always=True)
132137
def _set_hierarchy(cls, v: str, values) -> str:
133138
if not v:
@@ -200,7 +205,7 @@ def traverse_down(self) -> List[Category]:
200205
The list is ordered starting with the current category, then its
201206
children, then the children's children etc.
202207
"""
203-
nodes_to_visit = [self]
208+
nodes_to_visit: List[Category] = [self]
204209
all_children = []
205210
while nodes_to_visit:
206211
current_node = nodes_to_visit.pop()
@@ -232,7 +237,7 @@ def find_child(self, node_name: str) -> Union[Category, None]:
232237
KeyError
233238
If the category cannot be found.
234239
"""
235-
nodes_to_visit = [self]
240+
nodes_to_visit: List[Category] = [self]
236241
while nodes_to_visit:
237242
current_node = nodes_to_visit.pop()
238243
if current_node.name == node_name:
@@ -470,7 +475,7 @@ def from_list(cls, lst: List[str]) -> Category:
470475
return new_cat
471476

472477

473-
ListOrDictOfCategories = Union[List[Category], Dict[str, Category]]
478+
CategoryContainer = TypeVar("CategoryContainer", List[Category], Dict[str, Category])
474479

475480

476481
def create_categories_from_hierarchy(hierarchy: str) -> Category:
@@ -489,8 +494,8 @@ def create_categories_from_hierarchy(hierarchy: str) -> Category:
489494

490495
def add_categories_to_container(
491496
new_category: Category,
492-
categories: ListOrDictOfCategories,
493-
) -> ListOrDictOfCategories:
497+
categories: CategoryContainer,
498+
) -> CategoryContainer:
494499
"""Add ``new_category`` to ``categories`` after first creating necessary
495500
hierarchy.
496501
@@ -511,29 +516,21 @@ def add_categories_to_container(
511516
The new iterable, now containing ``new_category``
512517
"""
513518
# Add categories in hierarchy
514-
categories_is_dict = isinstance(categories, dict)
515519
if new_category.hierarchy != new_category.name or new_category.children:
516520
# Get the root category in the chain
517-
root = new_category.traverse_up()[-1]
521+
new_category = new_category.traverse_up()[-1]
518522

519-
iterator = categories.values() if categories_is_dict else categories
523+
iterator = categories.values() if isinstance(categories, dict) else categories
520524

521525
# Check if the root category already exists in the categories container
522526
for category in iterator:
523-
category: Category # Type hint
524-
success = category.merge(root)
527+
success = category.merge(new_category)
525528
if success:
526-
break
527-
else:
528-
# If the category doesn't exist, add it to the categories container
529-
if categories_is_dict:
530-
categories[root.name] = root
531-
else:
532-
categories.append(root)
529+
return categories
530+
531+
if isinstance(categories, dict):
532+
categories[new_category.name] = new_category
533533
else:
534-
if categories_is_dict:
535-
categories[new_category.name] = new_category
536-
else:
537-
categories.append(new_category)
534+
categories.append(new_category)
538535

539536
return categories

quiffen/core/qif.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import io
55
from enum import Enum
66
from pathlib import Path
7-
from typing import Any, Dict, List, Optional, Set, Union
7+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Union
88

99
from pydantic.types import FilePath
1010

@@ -17,13 +17,9 @@
1717
from quiffen.core.security import Security
1818
from quiffen.core.transaction import Transaction
1919

20-
try:
20+
if TYPE_CHECKING:
2121
import pandas as pd
2222

23-
PANDAS_INSTALLED = True
24-
except ModuleNotFoundError:
25-
PANDAS_INSTALLED = False
26-
2723
VALID_TRANSACTION_ACCOUNT_TYPES = [
2824
"!type:cash",
2925
"!type:bank",
@@ -203,7 +199,7 @@ def parse(
203199
if "!Type:Cat" in header_line:
204200
# Section contains category information
205201
new_category = Category.from_list(sanitised_section_lines)
206-
categories = add_categories_to_container( # type: ignore
202+
categories = add_categories_to_container(
207203
new_category,
208204
categories,
209205
)
@@ -325,7 +321,7 @@ def remove_account(self, account_name: str) -> Account:
325321

326322
def add_category(self, new_category: Category) -> None:
327323
"""Add a new category to the Qif object"""
328-
self.categories = add_categories_to_container( # type: ignore
324+
self.categories = add_categories_to_container(
329325
new_category,
330326
self.categories,
331327
)
@@ -576,10 +572,12 @@ def to_dataframe(
576572
pd.DataFrame
577573
The data as a Pandas DataFrame
578574
"""
579-
if not PANDAS_INSTALLED:
575+
try:
576+
import pandas as pd
577+
except ModuleNotFoundError as e:
580578
raise ModuleNotFoundError(
581-
"The pandas module must be installed to use this method"
582-
)
579+
"The pandas package is required to convert Qif objects to DataFrames. Please install it using `pip install quiffen[pandas]`."
580+
) from e
583581

584582
if ignore is None:
585583
ignore = []

quiffen/core/transaction.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -453,10 +453,14 @@ def from_list(
453453
if class_name:
454454
classes[class_name].add_category(category)
455455
elif line_code == "N":
456+
try:
457+
check_number: Union[int, str] = int(field_info)
458+
except ValueError:
459+
check_number = field_info
456460
if not splits:
457-
kwargs["check_number"] = field_info
461+
kwargs["check_number"] = check_number
458462
elif current_split:
459-
current_split.check_number = field_info
463+
current_split.check_number = check_number
460464
elif line_code == "F":
461465
kwargs["reimbursable_expense"] = field_info or True
462466
elif line_code == "1":

0 commit comments

Comments
 (0)