Skip to content

Commit 137f2a6

Browse files
authored
Merge pull request #877 from cordada/deploy/v0.54.0
Deploy release v0.54.0
2 parents b5c076e + 420c517 commit 137f2a6

File tree

10 files changed

+104
-20
lines changed

10 files changed

+104
-20
lines changed

.bumpversion.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 0.53.0
2+
current_version = 0.54.0
33
commit = True
44
tag = False
55
message = chore: Bump version from {current_version} to {new_version}

HISTORY.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# History
22

3+
## 0.54.0 (2025-09-09)
4+
5+
- (PR #871, 2025-09-09) rut: Use class variables for exception messages raised by `Rut`
6+
- (PR #873, 2025-09-09) chore(deps): Bump the python-development group across 1 directory with 2 updates
7+
- (PR #874, 2025-09-09) extras: Add option to validate RUT DV to Django model field `RutField`
8+
- (PR #875, 2025-09-09) extras: Refactor `dj_form_fields.RutField.to_python()`
9+
- (PR #872, 2025-09-09) chore(deps): Bump django from 4.2.23 to 4.2.24
10+
311
## 0.53.0 (2025-09-02)
412

513
- (PR #862, 2025-08-28) chore(deps): Bump the github-actions-production group with 5 updates

requirements-dev.in

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
black==25.1.0
88
build==1.3.0
99
bumpversion==0.5.3
10-
coverage==7.10.4
10+
coverage==7.10.6
1111
flake8==7.3.0
1212
isort==6.0.1
1313
mypy==1.17.1
1414
pip-tools==7.4.1
1515
tox==4.27.0
16-
twine==6.1.0
16+
twine==6.2.0
1717
types-jsonschema==4.25.0.20250809
1818
types-lxml==2025.3.30
1919
types-pytz==2025.2.0.20250809

requirements-dev.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ click==8.0.3
4040
# pip-tools
4141
colorama==0.4.6
4242
# via tox
43-
coverage==7.10.4
43+
coverage==7.10.6
4444
# via -r requirements-dev.in
4545
cryptography==45.0.4
4646
# via
@@ -164,7 +164,7 @@ tomli==2.2.1
164164
# tox
165165
tox==4.27.0
166166
# via -r requirements-dev.in
167-
twine==6.1.0
167+
twine==6.2.0
168168
# via -r requirements-dev.in
169169
types-html5lib==1.1.11.20241018
170170
# via types-lxml

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ cryptography==45.0.4
2323
# signxml
2424
defusedxml==0.7.1
2525
# via -r requirements.in
26-
django==4.2.23
26+
django==4.2.24
2727
# via
2828
# -r requirements.in
2929
# django-filter

src/cl_sii/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
55
"""
66

7-
__version__ = '0.53.0'
7+
__version__ = '0.54.0'

src/cl_sii/extras/dj_form_fields.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def to_python(self, value: Optional[object]) -> Optional[Rut]:
6666
if (
6767
converted_value is not None
6868
and self.validate_dv
69-
and Rut.calc_dv(converted_value.digits) != converted_value.dv
69+
and not converted_value.validate_dv(raise_exception=False)
7070
):
7171
raise django.core.exceptions.ValidationError(
7272
self.error_messages['invalid_dv'],

src/cl_sii/extras/dj_model_fields.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
except ImportError as exc: # pragma: no cover
99
raise ImportError("Package 'Django' is required to use this module.") from exc
1010

11-
from typing import Any, Optional, Tuple
11+
from typing import Any, ClassVar, Optional, Tuple
1212

1313
import django.core.exceptions
1414
import django.db.models
@@ -41,7 +41,6 @@ class RutField(django.db.models.Field):
4141
4242
"""
4343

44-
# TODO: add option to validate that "digito verificador" is correct.
4544
# TODO: implement method 'formfield'. Probably a copy of 'CharField.formfield' is fine.
4645

4746
description = 'RUT for SII (Chile)'
@@ -50,8 +49,18 @@ class RutField(django.db.models.Field):
5049
'invalid_dv': "\"digito verificador\" of RUT '%(value)s' is incorrect.",
5150
}
5251
empty_strings_allowed = False
52+
validate_dv_by_default: ClassVar[bool] = False
53+
54+
def __init__(
55+
self,
56+
verbose_name: Optional[str] = None,
57+
name: Optional[str] = None,
58+
validate_dv: bool = validate_dv_by_default,
59+
*args: Any,
60+
**kwargs: Any,
61+
) -> None:
62+
self.validate_dv = validate_dv
5363

54-
def __init__(self, *args: Any, **kwargs: Any) -> None:
5564
# note: the value saved to the DB will always be in canonical format.
5665
db_column_max_length = cl_sii.rut.constants.RUT_CANONICAL_MAX_LENGTH
5766

@@ -62,7 +71,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
6271
raise ValueError("This field does not allow customization of 'max_length'.")
6372

6473
kwargs['max_length'] = db_column_max_length
65-
super().__init__(*args, **kwargs)
74+
super().__init__(verbose_name, name, *args, **kwargs)
6675

6776
def deconstruct(self) -> Tuple[str, str, Any, Any]:
6877
"""
@@ -71,6 +80,10 @@ def deconstruct(self) -> Tuple[str, str, Any, Any]:
7180
# note: this override is necessary because we have a custom constructor.
7281

7382
name, path, args, kwargs = super().deconstruct()
83+
84+
if self.validate_dv != self.validate_dv_by_default:
85+
kwargs['validate_dv'] = self.validate_dv
86+
7487
del kwargs['max_length']
7588

7689
return name, path, args, kwargs
@@ -144,8 +157,19 @@ def to_python(self, value: Optional[object]) -> Optional[Rut]:
144157
converted_value = value
145158
else:
146159
try:
147-
converted_value = Rut(value, validate_dv=False) # type: ignore
148-
except (AttributeError, TypeError, ValueError):
160+
converted_value = Rut(value, validate_dv=self.validate_dv) # type: ignore
161+
except (AttributeError, TypeError, ValueError) as exc:
162+
if (
163+
isinstance(exc, ValueError)
164+
and exc.args
165+
and exc.args[0] == Rut.INVALID_DV_ERROR_MESSAGE
166+
):
167+
raise django.core.exceptions.ValidationError(
168+
self.error_messages['invalid_dv'],
169+
code='invalid_dv',
170+
params={'value': value},
171+
)
172+
149173
raise django.core.exceptions.ValidationError(
150174
self.error_messages['invalid'],
151175
code='invalid',

src/cl_sii/rut/__init__.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import itertools
1616
import random
1717
import re
18+
from typing import ClassVar
1819

1920
from . import constants
2021

@@ -52,6 +53,21 @@ class Rut:
5253
5354
"""
5455

56+
INVALID_TYPE_ERROR_MESSAGE: ClassVar[str] = "Invalid type."
57+
"""
58+
Error message used when the input type is not one of the accepted ones.
59+
"""
60+
61+
INVALID_RUT_ERROR_MESSAGE: ClassVar[str] = "Syntactically invalid RUT."
62+
"""
63+
Error message used when the input is not a syntactically valid RUT.
64+
"""
65+
66+
INVALID_DV_ERROR_MESSAGE: ClassVar[str] = "RUT's \"digito verificador\" is incorrect."
67+
"""
68+
Error message used when the RUT's "digito verificador" is incorrect.
69+
"""
70+
5571
def __init__(self, value: str | Rut, validate_dv: bool = False) -> None:
5672
"""
5773
Constructor.
@@ -64,17 +80,15 @@ def __init__(self, value: str | Rut, validate_dv: bool = False) -> None:
6480
:raises TypeError:
6581
6682
"""
67-
invalid_rut_msg = "Syntactically invalid RUT."
68-
6983
if isinstance(value, Rut):
7084
value = value.canonical
7185
if not isinstance(value, str):
72-
raise TypeError("Invalid type.")
86+
raise TypeError(self.INVALID_TYPE_ERROR_MESSAGE)
7387

7488
clean_value = Rut.clean_str(value)
7589
match_obj = constants.RUT_CANONICAL_STRICT_REGEX.match(clean_value)
7690
if match_obj is None:
77-
raise ValueError(invalid_rut_msg, value)
91+
raise ValueError(self.INVALID_RUT_ERROR_MESSAGE, value)
7892

7993
match_groups = match_obj.groupdict()
8094
self._digits = match_groups['digits']
@@ -151,7 +165,7 @@ def validate_dv(self, raise_exception: bool = False) -> bool:
151165
"""
152166
is_valid = self.calc_dv(self._digits) == self._dv
153167
if not is_valid and raise_exception:
154-
raise ValueError("RUT's \"digito verificador\" is incorrect.", self.canonical)
168+
raise ValueError(self.INVALID_DV_ERROR_MESSAGE, self.canonical)
155169
return is_valid
156170

157171
############################################################################

src/tests/test_extras_dj_model_fields.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
11
import unittest
2+
import unittest.mock
23

3-
import django.db.models # noqa: F401
4+
import django.core.exceptions
5+
import django.db.models
46

57
from cl_sii.extras.dj_model_fields import Rut, RutField
68

79

810
class RutFieldTest(unittest.TestCase):
911
valid_rut_canonical: str
1012
valid_rut_instance: Rut
13+
valid_rut_canonical_with_invalid_dv: str
1114
valid_rut_verbose_leading_zero_lowercase: str
15+
mock_model_instance: django.db.models.Model
1216

1317
@classmethod
1418
def setUpClass(cls) -> None:
1519
cls.valid_rut_canonical = '60803000-K'
1620
cls.valid_rut_instance = Rut(cls.valid_rut_canonical)
21+
cls.valid_rut_canonical_with_invalid_dv = '60803000-0'
1722
cls.valid_rut_verbose_leading_zero_lowercase = '060.803.000-k'
23+
cls.mock_model_instance = unittest.mock.create_autospec(
24+
django.db.models.Model, instance=True
25+
)
1826

1927
def test_get_prep_value_of_canonical_str(self) -> None:
2028
prepared_value = RutField().get_prep_value(self.valid_rut_canonical)
@@ -34,3 +42,33 @@ def test_get_prep_value_of_Rut(self) -> None:
3442
def test_get_prep_value_of_None(self) -> None:
3543
prepared_value = RutField().get_prep_value(None)
3644
self.assertIsNone(prepared_value)
45+
46+
def test_clean_value_of_rut_str_with_invalid_dv_if_validated(self) -> None:
47+
rut_field = RutField(validate_dv=True)
48+
with self.assertRaises(django.core.exceptions.ValidationError) as cm:
49+
rut_field.clean(self.valid_rut_canonical_with_invalid_dv, self.mock_model_instance)
50+
self.assertEqual(cm.exception.code, 'invalid_dv')
51+
52+
def test_clean_value_of_rut_str_with_invalid_dv_if_not_validated(self) -> None:
53+
rut_field = RutField(validate_dv=False)
54+
cleaned_value = rut_field.clean(
55+
self.valid_rut_canonical_with_invalid_dv, self.mock_model_instance
56+
)
57+
self.assertIsInstance(cleaned_value, Rut)
58+
self.assertEqual(cleaned_value.canonical, self.valid_rut_canonical_with_invalid_dv)
59+
60+
def test_deconstruct_without_options(self) -> None:
61+
name, path, args, kwargs = RutField().deconstruct()
62+
self.assertEqual(path, 'cl_sii.extras.dj_model_fields.RutField')
63+
self.assertEqual(args, [])
64+
self.assertEqual(kwargs, {})
65+
66+
def test_deconstruct_with_option_validate_dv_enabled(self) -> None:
67+
name, path, args, kwargs = RutField(validate_dv=True).deconstruct()
68+
self.assertEqual(args, [])
69+
self.assertEqual(kwargs, {'validate_dv': True})
70+
71+
def test_deconstruct_with_option_validate_dv_disabled(self) -> None:
72+
name, path, args, kwargs = RutField(validate_dv=False).deconstruct()
73+
self.assertEqual(args, [])
74+
self.assertEqual(kwargs, {})

0 commit comments

Comments
 (0)