Skip to content

Commit db3ee19

Browse files
chore: add uv lockfile, ruff, and improve ci
this commit: - add a lockfile to make it easier to test with consistent dependencies. - fix a bunch of issues that ruff detected (ruff is a bit opinionated...) - add a matrix strategy to the CI to detect breakages in previous versions of python. This is especially important for the folks using debian or other stable distros.
1 parent 3cd0607 commit db3ee19

11 files changed

Lines changed: 1022 additions & 70 deletions

File tree

.github/workflows/beancount.yml

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
11
name: beancount
22

33
on:
4-
[push, pull_request]
4+
push:
5+
branches: ['main', 'master']
6+
pull_request:
57

68
jobs:
79
test:
810
runs-on: ubuntu-latest
911
strategy:
1012
fail-fast: false
13+
matrix:
14+
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
1115
steps:
12-
- uses: actions/checkout@v2
13-
- uses: actions/setup-python@v2
16+
- uses: actions/checkout@v6
17+
- name: Set up uv
18+
uses: astral-sh/setup-uv@v4
1419
with:
15-
python-version: '3.9'
16-
- run: pip install -r requirements_dev.txt
17-
- run: pylint beanprice
18-
- run: pytest beanprice
19-
- run: mypy beanprice
20+
python-version: ${{ matrix.python-version }}
21+
- run: uv sync --group dev
22+
- run: uv run ruff check beanprice
23+
- run: uv run pylint beanprice
24+
- run: uv run pytest beanprice
25+
- run: uv run mypy beanprice

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ Otherwise read the source.
2020
To install beanprice, run:
2121

2222
```shell
23+
# using pip
2324
pip install git+https://github.com/beancount/beanprice.git
25+
26+
# using uv
27+
uv add git+https://github.com/beancount/beanprice.git
2428
```
2529

2630
You can fetch the latest price of a stock by running:

beanprice/price.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import sys
1616
import logging
1717
from concurrent import futures
18-
from typing import Any, Dict, List, Optional, NamedTuple, Tuple
18+
from typing import Any, Optional, NamedTuple
1919
import diskcache
2020

2121
from dateutil import tz
@@ -60,7 +60,7 @@ class DatedPrice(NamedTuple):
6060
base: Optional[str]
6161
quote: Optional[str]
6262
date: Optional[datetime.date]
63-
sources: List[PriceSource]
63+
sources: list[PriceSource]
6464

6565

6666
# The Python package where the default sources are found.
@@ -102,7 +102,7 @@ def format_dated_price_str(dprice: DatedPrice) -> str:
102102
)
103103

104104

105-
def parse_source_map(source_map_spec: str) -> Dict[str, List[PriceSource]]:
105+
def parse_source_map(source_map_spec: str) -> dict[str, list[PriceSource]]:
106106
"""Parse a source map specification string.
107107
108108
Source map specifications allow the specification of multiple sources for
@@ -134,7 +134,7 @@ def parse_source_map(source_map_spec: str) -> Dict[str, List[PriceSource]]:
134134
Raises:
135135
ValueError: If an invalid pattern has been specified.
136136
"""
137-
source_map: Dict[str, List[PriceSource]] = collections.defaultdict(list)
137+
source_map: dict[str, list[PriceSource]] = collections.defaultdict(list)
138138
for source_list_spec in re.split("[ ;]", source_map_spec):
139139
match = re.match("({}):(.*)$".format(amount.CURRENCY_RE), source_list_spec)
140140
if not match:
@@ -202,7 +202,7 @@ def import_source(module_name: str):
202202
def find_currencies_declared(
203203
entries: data.Entries,
204204
date: Optional[datetime.date] = None,
205-
) -> List[Tuple[str, str, List[PriceSource]]]:
205+
) -> list[tuple[str, str, list[PriceSource]]]:
206206
"""Return currencies declared in Commodity directives.
207207
208208
If a 'price' metadata field is provided, include all the quote currencies
@@ -639,8 +639,8 @@ def fetch_price(dprice: DatedPrice, swap_inverted: bool = False) -> Optional[dat
639639

640640

641641
def filter_redundant_prices(
642-
price_entries: List[data.Price], existing_entries: List[data.Price], diffs: bool = False
643-
) -> Tuple[List[data.Price], List[data.Price]]:
642+
price_entries: list[data.Price], existing_entries: list[data.Price], diffs: bool = False
643+
) -> tuple[list[data.Price], list[data.Price]]:
644644
"""Filter out new entries that are redundant from an existing set.
645645
646646
If the price differs, we override it with the new entry only on demand. This
@@ -663,8 +663,8 @@ def filter_redundant_prices(
663663
for entry in existing_entries
664664
if isinstance(entry, data.Price)
665665
}
666-
filtered_prices: List[data.Price] = []
667-
ignored_prices: List[data.Price] = []
666+
filtered_prices: list[data.Price] = []
667+
ignored_prices: list[data.Price] = []
668668
for entry in price_entries:
669669
key = (entry.date, entry.currency)
670670
if key in existing_prices:
@@ -680,9 +680,9 @@ def filter_redundant_prices(
680680
return filtered_prices, ignored_prices
681681

682682

683-
def process_args() -> Tuple[
683+
def process_args() -> tuple[
684684
argparse.Namespace,
685-
List[DatedPrice],
685+
list[DatedPrice],
686686
data.Directives,
687687
Optional[Any],
688688
]:
@@ -887,7 +887,7 @@ def process_args() -> Tuple[
887887
if args.expressions:
888888
# Interpret the arguments as price sources.
889889
for source_str in args.sources:
890-
psources: List[PriceSource] = []
890+
psources: list[PriceSource] = []
891891
try:
892892
psource_map = parse_source_map(source_str)
893893
except ValueError:
@@ -940,7 +940,7 @@ def process_args() -> Tuple[
940940
)
941941
continue
942942
logging.info('Loading "%s"', filename)
943-
entries, errors, options_map = loader.load_file(filename, log_errors=sys.stderr)
943+
entries, _errors, options_map = loader.load_file(filename, log_errors=sys.stderr)
944944
if dcontext is None:
945945
dcontext = options_map["dcontext"]
946946
for date in dates:

beanprice/price_test.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,20 +209,20 @@ def test_explicit_file__badcontents(self, filename):
209209
2015-01-01 open USD ;; Error
210210
"""
211211
with test_utils.capture("stderr"):
212-
args, jobs, _, __ = run_with_args(price.process_args, ["--no-cache", filename])
212+
_args, jobs, _, __ = run_with_args(price.process_args, ["--no-cache", filename])
213213
self.assertEqual([], jobs)
214214

215215
def test_filename_exists(self):
216216
with tempfile.NamedTemporaryFile("w") as tmpfile:
217217
with test_utils.capture("stderr"):
218-
args, jobs, _, __ = run_with_args(
218+
_args, jobs, _, __ = run_with_args(
219219
price.process_args, ["--no-cache", tmpfile.name]
220220
)
221221
self.assertEqual([], jobs) # Empty file.
222222

223223
def test_expressions(self):
224224
with test_utils.capture("stderr"):
225-
args, jobs, _, __ = run_with_args(
225+
_args, jobs, _, __ = run_with_args(
226226
price.process_args, ["--no-cache", "-e", "USD:yahoo/AAPL"]
227227
)
228228
self.assertEqual(

beanprice/source.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import datetime
1616
from decimal import Decimal
17-
from typing import List, Optional, NamedTuple
17+
from typing import Optional, NamedTuple
1818

1919

2020
# A record that contains data for a price fetched from a source.
@@ -93,7 +93,7 @@ def get_historical_price(
9393

9494
def get_prices_series(
9595
self, ticker: str, time_begin: datetime.datetime, time_end: datetime.datetime
96-
) -> Optional[List[SourcePrice]]:
96+
) -> Optional[list[SourcePrice]]:
9797
"""Return the historical daily price series between two dates.
9898
9999
Note that weekends don't have any prices, so there's no guarantee that

beanprice/sources/coincap.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from datetime import datetime, timezone, timedelta
1616
import math
1717
from decimal import Decimal
18-
from typing import List, Optional, Dict
18+
from typing import Optional
1919
import requests
2020
from beanprice import source
2121

@@ -26,7 +26,7 @@ class CoincapError(ValueError):
2626
"An error from the Coincap importer."
2727

2828

29-
def get_asset_list() -> List[Dict[str, str]]:
29+
def get_asset_list() -> list[dict[str, str]]:
3030
"""
3131
Get list of currencies supported by Coincap. Returned is a list with
3232
elements with many properties, including "id", representing the Coincap id,
@@ -85,7 +85,7 @@ def get_latest_price(base_currency: str) -> source.SourcePrice:
8585

8686
def get_price_series(
8787
base_currency_id: str, time_begin: datetime, time_end: datetime
88-
) -> List[source.SourcePrice]:
88+
) -> list[source.SourcePrice]:
8989
path = f"assets/{base_currency_id}/history"
9090
params = {
9191
"interval": "d1",
@@ -129,5 +129,5 @@ def get_historical_price(
129129

130130
def get_prices_series(
131131
self, ticker: str, time_begin: datetime, time_end: datetime
132-
) -> List[source.SourcePrice]:
132+
) -> list[source.SourcePrice]:
133133
return get_price_series(resolve_currency_id(ticker), time_begin, time_end)

beanprice/sources/ecbrates_test.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,17 @@ def response(contents, status_code=requests.codes.ok):
3535

3636
class ECBRatesErrorFetcher(unittest.TestCase):
3737
def test_error_invalid_ticker(self):
38-
with self.assertRaises(ValueError) as exc:
38+
with self.assertRaises(ValueError):
3939
ecbrates.Source().get_latest_price("INVALID")
4040

4141
def test_error_network(self):
4242
with response("Foobar", 404):
43-
with self.assertRaises(ValueError) as exc:
43+
with self.assertRaises(ValueError):
4444
ecbrates.Source().get_latest_price("EUR-SEK")
4545

4646
def test_empty_response(self):
4747
with response("", 200):
48-
with self.assertRaises(ecbrates.ECBRatesError) as exc:
48+
with self.assertRaises(ecbrates.ECBRatesError):
4949
ecbrates.Source().get_latest_price("EUR-SEK")
5050

5151
def test_valid_response(self):

beanprice/sources/yahoo.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
from datetime import datetime, timedelta, timezone
2222
from decimal import Decimal
23-
from typing import Any, Dict, List, Optional, Tuple, Union
23+
from typing import Any, Optional, Union
2424

2525
from curl_cffi import requests
2626

@@ -31,7 +31,7 @@ class YahooError(ValueError):
3131
"An error from the Yahoo API."
3232

3333

34-
def parse_response(response: requests.models.Response) -> Dict:
34+
def parse_response(response: requests.models.Response) -> dict:
3535
"""Process as response from Yahoo.
3636
3737
Raises:
@@ -62,7 +62,7 @@ def parse_response(response: requests.models.Response) -> Dict:
6262
}
6363

6464

65-
def parse_currency(result: Dict[str, Any]) -> Optional[str]:
65+
def parse_currency(result: dict[str, Any]) -> Optional[str]:
6666
"""Infer the currency from the result."""
6767
if "market" not in result:
6868
return None
@@ -81,13 +81,13 @@ def get_price_series(
8181
time_begin: datetime,
8282
time_end: datetime,
8383
session: requests.Session,
84-
) -> Tuple[List[Tuple[datetime, Decimal]], str]:
84+
) -> tuple[list[tuple[datetime, Decimal]], str]:
8585
"""Return a series of timestamped prices."""
8686

8787
if requests is None:
8888
raise YahooError("You must install the 'requests' library.")
8989
url = "https://query1.finance.yahoo.com/v8/finance/chart/{}".format(ticker)
90-
payload: Dict[str, Union[int, str]] = {
90+
payload: dict[str, Union[int, str]] = {
9191
"period1": int(time_begin.timestamp()),
9292
"period2": int(time_end.timestamp()),
9393
"interval": "1d",
@@ -203,7 +203,7 @@ def get_historical_price(
203203

204204
def get_daily_prices(
205205
self, ticker: str, time_begin: datetime, time_end: datetime
206-
) -> Optional[List[source.SourcePrice]]:
206+
) -> Optional[list[source.SourcePrice]]:
207207
"""See contract in beanprice.source.Source."""
208208
series, currency = get_price_series(ticker, time_begin, time_end, self.session)
209209
return [source.SourcePrice(price, time, currency) for time, price in series]

experiments/dividends/download_dividends.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
from datetime import date as Date
88
from decimal import Decimal
9-
from typing import List, Tuple
109
import argparse
1110
import csv
1211
import datetime
@@ -19,7 +18,7 @@
1918

2019
def download_dividends(
2120
instrument: str, start_date: Date, end_date: Date
22-
) -> List[Tuple[Date, Decimal]]:
21+
) -> list[tuple[Date, Decimal]]:
2322
"""Download a list of dividends issued over a time interval."""
2423
tim = datetime.time()
2524
payload = {

pyproject.toml

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[build-system]
2-
requires = ['setuptools']
2+
requires = ['setuptools', 'uv']
33
build-backend = 'setuptools.build_meta'
44

55
[project]
@@ -48,7 +48,8 @@ issues = 'https://github.com/beancount/beanprice/issues'
4848
dev = [
4949
"pytest>=8.3.5",
5050
"pylint==3.3.3",
51-
"mypy==1.14.1"
51+
"mypy==1.14.1",
52+
"ruff>=0.9.0"
5253
]
5354

5455
[tool.setuptools.packages]
@@ -62,34 +63,9 @@ exclude_also = [
6263
'if typing.TYPE_CHECKING:',
6364
]
6465

65-
[tool.ruff]
66-
line-length = 92
67-
target-version = 'py39'
68-
69-
[tool.ruff.lint]
70-
select = ['E', 'F', 'W', 'UP', 'B', 'C4', 'PL', 'RUF']
71-
72-
# TODO(blais): Review these ignores.
66+
# pylint and ruff are not aligned on this... maybe removing pylint would solve the problem.
7367
ignore = [
74-
'RUF013',
75-
'RUF005',
76-
'PLW0603',
77-
'UP014',
78-
'UP031',
79-
'B007',
80-
'B905',
81-
'C408',
82-
'E731',
83-
'PLR0911',
84-
'PLR0912',
85-
'PLR0913',
86-
'PLR0915',
87-
'PLR1714',
88-
'PLR2004',
89-
'PLW2901',
90-
'RUF012',
91-
'UP007',
92-
'UP032',
68+
'C0301',
9369
]
9470

9571
[tool.mypy]

0 commit comments

Comments
 (0)