Skip to content

Commit 0461fa1

Browse files
fix: update code to dependency updates (#1768)
CI was failing because several dependencies (with no major version pin) have introduced breaking changes or changed behavior. - `chardet v7` has different character encoding detections that chardet v5. Several tests were broken. - some tests were simply testing the (wrong) detection from chardet. They are now testing the correct/most likely encoding of the file (UTF-8). - test files are really short, with a unique non-ascii character. Chardet's confidence is therefore very low, and it cannot distinguish several encodings. Some tests were made less strict ("Check that the non-ascii is correctly decoded" instead of "check the exact encoding"), and the sample size to detect encoding has been augmented (x10). - there was a dependency on `pytz`, which was a (former) transitive dependency (from `pandas` maybe ?). It has been replaced with `datetime`. - Pandas v2 to v3 changes its `dtypes` - adapt to new dtypes collection type - make the tests accept former and new dtypes alongside - deal with new "nan" handling - adapt to changes of `pytest-cov` v7. - pin TatSu < 5.15 - pin virtualenv < 21 for hatch ([hatch issue](pypa/hatch#2193), still applies to 3.8 and 3.9) Additionnaly : - add CI tests for python 3.13 and 3.14 - depreciation warning for python 3.8 and 3.9 - CI for py3.8 takes ages, so I removed it.
1 parent be08dcf commit 0461fa1

15 files changed

Lines changed: 99 additions & 57 deletions

File tree

.github/workflows/general.yaml

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,17 @@ jobs:
2121
strategy:
2222
fail-fast: false
2323
matrix:
24-
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
24+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
2525
steps:
2626
- name: Checkout repository
27-
uses: actions/checkout@v4
27+
uses: actions/checkout@v5
2828
- name: Install Python
29-
uses: actions/setup-python@v5
29+
uses: actions/setup-python@v6
3030
with:
3131
python-version: ${{ matrix.python-version }}
3232
- name: Prepare environment
33-
run: pip3 install hatch
33+
# https://github.com/pypa/hatch/issues/2193
34+
run: pip3 install hatch 'virtualenv<21'
3435
- name: Prepare variables
3536
run: cp .env.example .env
3637
- name: Prepare secrets
@@ -78,9 +79,9 @@ jobs:
7879
runs-on: macos-14
7980
steps:
8081
- name: Checkout repository
81-
uses: actions/checkout@v4
82+
uses: actions/checkout@v5
8283
- name: Install Python
83-
uses: actions/setup-python@v5
84+
uses: actions/setup-python@v6
8485
with:
8586
python-version: "3.10"
8687
- name: Set up postgresql
@@ -100,9 +101,9 @@ jobs:
100101
runs-on: windows-latest
101102
steps:
102103
- name: Checkout repository
103-
uses: actions/checkout@v4
104+
uses: actions/checkout@v5
104105
- name: Install Python
105-
uses: actions/setup-python@v5
106+
uses: actions/setup-python@v6
106107
with:
107108
python-version: "3.10"
108109
- name: Prepare environment
@@ -119,9 +120,9 @@ jobs:
119120
runs-on: ubuntu-latest
120121
steps:
121122
- name: Checkout repository
122-
uses: actions/checkout@v4
123+
uses: actions/checkout@v5
123124
- name: Install Python
124-
uses: actions/setup-python@v5
125+
uses: actions/setup-python@v6
125126
with:
126127
python-version: "3.10"
127128
- name: Prepare environment
@@ -145,9 +146,9 @@ jobs:
145146
needs: [test-linux, test-macos, test-windows]
146147
steps:
147148
- name: Checkout repository
148-
uses: actions/checkout@v4
149+
uses: actions/checkout@v5
149150
- name: Install Python
150-
uses: actions/setup-python@v5
151+
uses: actions/setup-python@v6
151152
with:
152153
python-version: "3.10"
153154
- name: Install dependencies

frictionless/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
import sys
2+
import warnings
3+
4+
if sys.version_info < (3, 10):
5+
warnings.warn(
6+
f"Python {sys.version_info.major}.{sys.version_info.minor} has reached end-of-life "
7+
"and is no longer supported. Please upgrade to Python 3.10 or later.",
8+
DeprecationWarning,
9+
stacklevel=2,
10+
)
11+
112
from .actions import convert as convert
213
from .actions import describe as describe
314
from .actions import extract as extract

frictionless/__main__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1+
import warnings
2+
13
from .console import console
24

5+
warnings.filterwarnings("default", category=DeprecationWarning, module="frictionless")
6+
7+
38
if __name__ == "__main__":
49
console(prog_name="frictionless")

frictionless/conftest.py

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

33
import pytest
44
import sqlalchemy as sa
5-
from pytest_cov.embed import cleanup_on_sigterm
65

76
from frictionless import platform
87

@@ -11,8 +10,13 @@
1110

1211
# Cleanups
1312

13+
try:
14+
# For python 3.8 only, that does not support pytest_cov v7
15+
from pytest_cov.embed import cleanup_on_sigterm
1416

15-
cleanup_on_sigterm()
17+
cleanup_on_sigterm()
18+
except ImportError:
19+
pass
1620

1721

1822
# Fixtures

frictionless/detector/detector.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,15 @@ def detect_encoding(
216216
encoding = detector.result["encoding"] or settings.DEFAULT_ENCODING
217217
confidence = detector.result["confidence"] or 0
218218
if confidence < self.encoding_confidence:
219+
# low confidence, so we try UTF8
220+
# If decoding fails, we fallback to the detected encoding
221+
# despite the low-confidence
222+
detected = encoding
219223
encoding = settings.DEFAULT_ENCODING
224+
try:
225+
buffer.decode(encoding)
226+
except UnicodeDecodeError:
227+
encoding = detected
220228
if encoding == "ascii":
221229
encoding = settings.DEFAULT_ENCODING
222230

@@ -329,7 +337,7 @@ def detect_schema(
329337

330338
# Handle name/empty
331339
for index, name in enumerate(names):
332-
names[index] = name or f"field{index+1}"
340+
names[index] = name or f"field{index + 1}"
333341

334342
# Deduplicate names
335343
if len(names) != len(set(names)):

frictionless/formats/markdown/mapper.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def dicts_to_markdown_table(dicts: List[Dict[str, Any]], **kwargs: Any) -> str:
9999
if kwargs:
100100
dicts = [filter_dict(x, **kwargs) for x in dicts]
101101
df = platform.pandas.DataFrame(dicts)
102-
return df.where(df.notnull(), None).to_markdown(index=False) # type: ignore
102+
return df.fillna("").to_markdown(index=False)
103103

104104

105105
def filter_dict(

frictionless/formats/pandas/__spec__/test_parser.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1-
from datetime import datetime, time
1+
from datetime import datetime, time, timezone
22
from decimal import Decimal
33

44
import isodate
55
import numpy as np
66
import pandas as pd
7-
import pytz
87
from dateutil.tz import tzoffset, tzutc
9-
from pandas.core.dtypes.common import is_datetime64_ns_dtype
8+
from pandas.api.types import is_datetime64_any_dtype
109

1110
from frictionless import Package, Schema, validate
1211
from frictionless.resources import TableResource
1312

13+
# Infer dtype from real DataFrame as the type is depending on pandas' version
14+
STRING_DTYPE = pd.DataFrame({"s": ["a"]}).dtypes["s"]
15+
1416
# Read
1517

1618

@@ -73,14 +75,14 @@ def test_pandas_parser_from_dataframe_with_primary_key_having_datetime():
7375
# Assert rows
7476
assert resource.read_rows() == [
7577
{
76-
"Date": datetime(2004, 1, 5, tzinfo=pytz.utc),
78+
"Date": datetime(2004, 1, 5, tzinfo=timezone.utc),
7779
"VIXClose": Decimal("17.49"),
7880
"VIXHigh": Decimal("18.49"),
7981
"VIXLow": Decimal("17.44"),
8082
"VIXOpen": Decimal("18.45"),
8183
},
8284
{
83-
"Date": datetime(2004, 1, 6, tzinfo=pytz.utc),
85+
"Date": datetime(2004, 1, 6, tzinfo=timezone.utc),
8486
"VIXClose": Decimal("16.73"),
8587
"VIXHigh": Decimal("17.67"),
8688
"VIXLow": Decimal("16.19"),
@@ -112,14 +114,14 @@ def test_pandas_parser_nan_in_integer_resource_column():
112114
]
113115
)
114116
df = res.to_pandas()
115-
assert all(df.dtypes.values == pd.array([pd.Int64Dtype(), float, object])) # type: ignore
117+
assert list(df.dtypes) == [pd.Int64Dtype(), np.dtype("float64"), STRING_DTYPE]
116118

117119

118120
def test_pandas_parser_nan_in_integer_csv_column():
119121
# see issue 1109
120122
res = TableResource(path="data/issue-1109.csv")
121123
df = res.to_pandas()
122-
assert all(df.dtypes.values == pd.array([pd.Int64Dtype(), float, object])) # type: ignore
124+
assert list(df.dtypes) == [pd.Int64Dtype(), np.dtype("float64"), STRING_DTYPE]
123125

124126

125127
def test_pandas_parser_write_types():
@@ -273,7 +275,7 @@ def test_pandas_parser_nan_with_field_type_information_1143():
273275
}
274276
res = TableResource.from_descriptor(descriptor)
275277
df = res.to_pandas()
276-
assert all(df.dtypes.values == pd.array([pd.Int64Dtype(), float, object])) # type: ignore
278+
assert list(df.dtypes) == [pd.Int64Dtype(), np.dtype("float64"), STRING_DTYPE]
277279

278280

279281
def test_pandas_parser_nan_without_field_type_information_1143():
@@ -291,7 +293,7 @@ def test_pandas_parser_nan_without_field_type_information_1143():
291293
}
292294
res = TableResource.from_descriptor(descriptor)
293295
df = res.to_pandas()
294-
assert all(df.dtypes.values == pd.array([object, object, object])) # type: ignore
296+
assert list(df.dtypes) == [STRING_DTYPE, STRING_DTYPE, STRING_DTYPE]
295297

296298

297299
def test_pandas_parser_preserve_datetime_field_type_1138():
@@ -311,7 +313,7 @@ def test_pandas_parser_preserve_datetime_field_type_1138():
311313
}
312314
resource = TableResource.from_descriptor(descriptor)
313315
df = resource.to_pandas()
314-
assert is_datetime64_ns_dtype(df.dtypes.values[1]) # type: ignore
316+
assert is_datetime64_any_dtype(df.dtypes.values[1]) # type: ignore
315317

316318

317319
def test_pandas_parser_test_issue_sample_data_1138():
@@ -338,7 +340,7 @@ def test_pandas_parser_test_issue_sample_data_1138():
338340
}
339341
resource = TableResource.from_descriptor(descriptor)
340342
df = resource.to_pandas()
341-
assert is_datetime64_ns_dtype(df.dtypes.values[0]) # type: ignore
343+
assert is_datetime64_any_dtype(df.dtypes.values[0]) # type: ignore
342344

343345

344346
def test_validate_package_with_in_code_resources_1245():

frictionless/formats/pandas/parser.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,7 @@ def write_row_stream(self, source: TableResource):
140140
value = float(value)
141141
# Convert to UTC for timezone aware datetime
142142
# From version 0.24 pandas preserves the dateutil object and doesn't by default
143-
# convert to "UTC" and fastparquet write raises error as it can't handle tzutc()
144-
# object
143+
# convert to "UTC"
145144
# https://github.com/pandas-dev/pandas/issues/25423#issuecomment-485784044
146145
if isinstance(value, datetime.datetime) and value.tzinfo:
147146
value = value.astimezone(datetime.timezone.utc)
Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
from __future__ import annotations
22

3+
from ... import types
34
from ...platform import platform
45
from ...resources import TableResource
56
from ...system import Parser
67
from .control import ParquetControl
78

89

910
class ParquetParser(Parser):
10-
"""JSONL parser implementation."""
11+
"""Parquet parser implementation."""
1112

1213
supported_types = [
1314
"array",
@@ -24,24 +25,30 @@ class ParquetParser(Parser):
2425

2526
# Read
2627

27-
def read_cell_stream_create(self):
28+
def read_cell_stream_create(self) -> types.ICellStream:
2829
control = ParquetControl.from_dialect(self.resource.dialect)
2930
handle = self.resource.normpath
3031
if self.resource.remote:
3132
handles = platform.pandas.io.common.get_handle( # type: ignore
3233
self.resource.normpath, "rb", is_text=False
3334
)
3435
handle = handles.handle
35-
file = platform.fastparquet.ParquetFile(handle)
36-
for group, df in enumerate(file.iter_row_groups(**control.to_python()), start=1):
37-
with TableResource(data=df, format="pandas") as resource:
38-
for line, cells in enumerate(resource.cell_stream, start=1):
39-
# Starting from second group we don't need a header row
40-
if group != 1 and line == 1:
41-
continue
42-
yield cells
36+
pq = platform.pyarrow_parquet
37+
table = pq.read_table(
38+
handle,
39+
columns=control.columns,
40+
filters=control.filters or None,
41+
)
42+
df = table.to_pandas(categories=control.categories or None)
43+
with TableResource(data=df, format="pandas") as resource:
44+
yield from resource.cell_stream
4345

4446
# Write
4547

4648
def write_row_stream(self, source: TableResource):
47-
platform.fastparquet.write(self.resource.normpath, source.to_pandas())
49+
import pyarrow as pa # type: ignore[reportMissingTypeStubs]
50+
51+
pq = platform.pyarrow_parquet
52+
df = source.to_pandas()
53+
table = pa.Table.from_pandas(df) # type: ignore[reportUnknownMemberType]
54+
pq.write_table(table, self.resource.normpath)

frictionless/formats/spss/__spec__/test_parser.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from frictionless.resources import TableResource
77

88
pytestmark = pytest.mark.skipif(
9-
platform.type == "darwin" or platform.python in ["3.10", "3.11", "3.12"],
9+
platform.type == "darwin"
10+
or platform.python in ["3.10", "3.11", "3.12", "3.13", "3.14"],
1011
reason="Not supported MacOS and Python3.10+",
1112
)
1213

0 commit comments

Comments
 (0)