Skip to content

Commit 9c27981

Browse files
committed
Extract config
- Instead of dictionary, create ARDConfig that encapsulates the configuration properties
1 parent 7515e69 commit 9c27981

File tree

13 files changed

+136
-54
lines changed

13 files changed

+136
-54
lines changed

pyard/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
# -*- coding: utf-8 -*-
2-
32
#
43
# py-ard
54
# Copyright (c) 2023 Be The Match operated by National Marrow Donor Program. All Rights Reserved.
@@ -21,10 +20,11 @@
2120
# > http://www.fsf.org/licensing/licenses/lgpl.html
2221
# > http://www.opensource.org/licenses/lgpl-license.php
2322
#
24-
from .constants import DEFAULT_CACHE_SIZE
2523

2624
# exports for `pyard`
2725
from .blender import blender as dr_blender
26+
from .config import ARDConfig
27+
from .constants import DEFAULT_CACHE_SIZE
2828
from .misc import get_imgt_db_versions as db_versions
2929

3030
__author__ = """NMDP Bioinformatics"""

pyard/ard.py

Lines changed: 14 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -26,21 +26,7 @@
2626
)
2727
from .misc import get_2field_allele, is_2_field_allele
2828
from .serology import SerologyMapping
29-
30-
default_config = {
31-
"reduce_serology": True,
32-
"reduce_v2": True,
33-
"reduce_3field": True,
34-
"reduce_P": True,
35-
"reduce_XX": True,
36-
"reduce_MAC": True,
37-
"reduce_shortnull": True,
38-
"ping": True,
39-
"verbose_log": False,
40-
"ARS_as_lg": False,
41-
"strict": True,
42-
"ignore_allele_with_suffixes": (),
43-
}
29+
from .config import ARDConfig
4430

4531

4632
class ARD(object):
@@ -57,9 +43,7 @@ def __init__(
5743
config: dict = None,
5844
):
5945
self._data_dir = data_dir
60-
self._config = default_config.copy()
61-
if config:
62-
self._config.update(config)
46+
self.config = ARDConfig.from_dict(config)
6347

6448
# Initialize specialized handlers
6549
self._initialize_handlers()
@@ -160,15 +144,15 @@ def _redux_allele(
160144
self, allele: str, redux_type: VALID_REDUCTION_TYPE, re_ping=True
161145
) -> str:
162146
"""Core allele reduction with ping logic"""
163-
if not self._config["strict"]:
147+
if not self.config.strict:
164148
allele = self._get_non_strict_allele(allele)
165149

166150
# Handle P/G suffixes
167151
if allele.endswith(("P", "G")) and redux_type in ["lg", "lgx", "G"]:
168152
allele = allele[:-1]
169153

170154
# Handle ping mode
171-
if self._config["ping"] and re_ping and redux_type in ("lg", "lgx", "U2"):
155+
if self.config.ping and re_ping and redux_type in ("lg", "lgx", "U2"):
172156
if allele in self.ars_mappings.p_not_g:
173157
not_g_allele = self.ars_mappings.p_not_g[allele]
174158
if redux_type == "lg":
@@ -206,8 +190,8 @@ def _redux_non_glstring(
206190
if "*" in allele:
207191
locus, fields = allele.split("*")
208192
# Handle ignored allele suffixes
209-
if self._config["ignore_allele_with_suffixes"]:
210-
if fields in self._config["ignore_allele_with_suffixes"]:
193+
if self.config.ignore_allele_with_suffixes:
194+
if fields in self.config.ignore_allele_with_suffixes:
211195
return allele
212196
if locus not in G_GROUP_LOCI:
213197
return allele
@@ -218,9 +202,7 @@ def _redux_non_glstring(
218202
return self.redux(allele, redux_type)
219203

220204
# Handle Serology
221-
if self._config["reduce_serology"] and self.serology_handler.is_serology(
222-
allele
223-
):
205+
if self.config.reduce_serology and self.serology_handler.is_serology(allele):
224206
alleles = self.serology_handler.get_alleles_from_serology(allele)
225207
if alleles:
226208
return self.redux("/".join(alleles), redux_type)
@@ -246,7 +228,7 @@ def _redux_non_glstring(
246228

247229
# Handle XX codes
248230
if (
249-
self._config["reduce_XX"]
231+
self.config.reduce_XX
250232
and code == "XX"
251233
and self.xx_handler.is_xx(allele, loc_antigen, code)
252234
):
@@ -256,17 +238,15 @@ def _redux_non_glstring(
256238
return reduced_alleles
257239

258240
# Handle MAC
259-
if self._config["reduce_MAC"] and code.isalpha():
241+
if self.config.reduce_MAC and code.isalpha():
260242
if self.mac_handler.is_mac(allele):
261243
alleles = self.mac_handler.get_alleles(code, loc_antigen)
262244
return self.redux("/".join(alleles), redux_type)
263245
else:
264246
raise InvalidMACError(f"{glstring} is an invalid MAC.")
265247

266248
# Handle short nulls
267-
if self._config["reduce_shortnull"] and self.shortnull_handler.is_shortnull(
268-
allele
269-
):
249+
if self.config.reduce_shortnull and self.shortnull_handler.is_shortnull(allele):
270250
return self.redux("/".join(self.shortnulls[allele]), redux_type)
271251

272252
redux_allele = self._redux_allele(allele, redux_type)
@@ -349,7 +329,7 @@ def _get_non_strict_allele(self, allele: str) -> str:
349329
if not self._is_allele_in_db(allele):
350330
for expr_char in expression_chars:
351331
if self._is_allele_in_db(allele + expr_char):
352-
if self._config["verbose_log"]:
332+
if self.config.verbose_log:
353333
print(f"{allele} is not valid. Using {allele}{expr_char}")
354334
allele = allele + expr_char
355335
break
@@ -387,12 +367,12 @@ def _is_valid(self, allele: str) -> bool:
387367
if not alphanum_allele.isalnum():
388368
return False
389369

390-
if self._config["ignore_allele_with_suffixes"]:
370+
if self.config.ignore_allele_with_suffixes:
391371
locus, fields = allele.split("*")
392-
if fields in self._config["ignore_allele_with_suffixes"]:
372+
if fields in self.config.ignore_allele_with_suffixes:
393373
return True
394374

395-
if not self._config["strict"]:
375+
if not self.config.strict:
396376
allele = self._get_non_strict_allele(allele)
397377

398378
if (

pyard/config.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from dataclasses import dataclass
4+
from typing import Tuple
5+
6+
7+
@dataclass
8+
class ARDConfig:
9+
"""Configuration class for ARD reduction settings"""
10+
11+
reduce_serology: bool = True
12+
reduce_v2: bool = True
13+
reduce_3field: bool = True
14+
reduce_P: bool = True
15+
reduce_XX: bool = True
16+
reduce_MAC: bool = True
17+
reduce_shortnull: bool = True
18+
ping: bool = True
19+
verbose_log: bool = False
20+
ARS_as_lg: bool = False
21+
strict: bool = True
22+
ignore_allele_with_suffixes: Tuple[str, ...] = ()
23+
24+
@classmethod
25+
def from_dict(cls, config_dict: dict) -> "ARDConfig":
26+
"""Create ARDConfig from dictionary"""
27+
if not config_dict:
28+
return cls()
29+
30+
# Filter only valid fields
31+
valid_fields = {f.name for f in cls.__dataclass_fields__.values()}
32+
filtered_config = {k: v for k, v in config_dict.items() if k in valid_fields}
33+
34+
return cls(**filtered_config)
35+
36+
def to_dict(self) -> dict:
37+
"""Convert ARDConfig to dictionary"""
38+
return {
39+
"reduce_serology": self.reduce_serology,
40+
"reduce_v2": self.reduce_v2,
41+
"reduce_3field": self.reduce_3field,
42+
"reduce_P": self.reduce_P,
43+
"reduce_XX": self.reduce_XX,
44+
"reduce_MAC": self.reduce_MAC,
45+
"reduce_shortnull": self.reduce_shortnull,
46+
"ping": self.ping,
47+
"verbose_log": self.verbose_log,
48+
"ARS_as_lg": self.ARS_as_lg,
49+
"strict": self.strict,
50+
"ignore_allele_with_suffixes": self.ignore_allele_with_suffixes,
51+
}
52+
53+
@property
54+
def serology_enabled(self) -> bool:
55+
return self.reduce_serology
56+
57+
@property
58+
def v2_enabled(self) -> bool:
59+
return self.reduce_v2
60+
61+
@property
62+
def field3_enabled(self) -> bool:
63+
return self.reduce_3field
64+
65+
@property
66+
def p_enabled(self) -> bool:
67+
return self.reduce_P
68+
69+
@property
70+
def xx_enabled(self) -> bool:
71+
return self.reduce_XX
72+
73+
@property
74+
def mac_enabled(self) -> bool:
75+
return self.reduce_MAC
76+
77+
@property
78+
def shortnull_enabled(self) -> bool:
79+
return self.reduce_shortnull
80+
81+
@property
82+
def ping_enabled(self) -> bool:
83+
return self.ping
84+
85+
@property
86+
def verbose_enabled(self) -> bool:
87+
return self.verbose_log
88+
89+
@property
90+
def ars_as_lg_enabled(self) -> bool:
91+
return self.ARS_as_lg
92+
93+
@property
94+
def strict_enabled(self) -> bool:
95+
return self.strict

pyard/handlers/allele_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,6 @@ def add_lg_suffix(self, redux_allele):
6868
[self.add_lg_suffix(allele) for allele in redux_allele.split("/")]
6969
)
7070
# Use 'ARS' suffix if configured, otherwise use 'g' suffix
71-
if self.ard._config["ARS_as_lg"]:
71+
if self.ard.config.ars_as_lg_enabled:
7272
return redux_allele + "ARS"
7373
return redux_allele + "g"

pyard/handlers/gl_string_processor.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def process_gl_string(
5050
validate_reduction_type(redux_type)
5151

5252
# Validate GL string structure if strict mode is enabled
53-
if self.ard._config["strict"]:
53+
if self.ard.config.strict_enabled:
5454
self.validate_gl_string(glstring)
5555

5656
# Handle GL string delimiters in order of precedence
@@ -115,7 +115,7 @@ def _sorted_unique_gl(self, gls: List[str], delim: str) -> str:
115115
non_empty_gls,
116116
key=functools.cmp_to_key(
117117
lambda a, b: self.ard.smart_sort_comparator(
118-
a, b, self.ard._config["ignore_allele_with_suffixes"]
118+
a, b, self.ard.config.ignore_allele_with_suffixes
119119
)
120120
),
121121
)
@@ -131,7 +131,7 @@ def _sorted_unique_gl(self, gls: List[str], delim: str) -> str:
131131
unique_gls,
132132
key=functools.cmp_to_key(
133133
lambda a, b: self.ard.smart_sort_comparator(
134-
a, b, self.ard._config["ignore_allele_with_suffixes"]
134+
a, b, self.ard.config.ignore_allele_with_suffixes
135135
)
136136
),
137137
)

pyard/handlers/shortnull_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def is_shortnull(self, allele: str) -> bool:
3838
True if the allele is a valid short null and short null reduction
3939
is enabled in configuration, False otherwise
4040
"""
41-
return allele in self.ard.shortnulls and self.ard._config["reduce_shortnull"]
41+
return allele in self.ard.shortnulls and self.ard.config.shortnull_enabled
4242

4343
def is_null(self, allele: str) -> bool:
4444
"""Check if allele is a null allele

pyard/handlers/v2_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def is_v2(self, allele: str) -> bool:
4444
"""
4545
# Check basic V2 format criteria
4646
matches_v2_format = (
47-
self.ard._config["reduce_v2"] # V2 reduction must be enabled
47+
self.ard.config.v2_enabled # V2 reduction must be enabled
4848
and "*" in allele # Must have locus separator
4949
and ":" not in allele # Must not have field separators (V3 feature)
5050
and allele.split("*")[0]

pyard/reducers/lg_reducer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,6 @@ def _add_lg_suffix(self, redux_allele: str) -> str:
129129
)
130130

131131
# Add suffix based on configuration
132-
if self.ard._config["ARS_as_lg"]:
132+
if self.ard.config.ars_as_lg_enabled:
133133
return redux_allele + "ARS"
134134
return redux_allele + "g"

tests/unit/handlers/test_allele_handler.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pytest
44
from unittest.mock import Mock, MagicMock
55

6+
from pyard import ARDConfig
67
from pyard.handlers.allele_handler import AlleleHandler
78

89

@@ -13,7 +14,7 @@ class TestAlleleHandler:
1314
def mock_ard(self):
1415
"""Create mock ARD instance"""
1516
ard = Mock()
16-
ard._config = {"ARS_as_lg": False}
17+
ard.config = ARDConfig.from_dict({"ARS_as_lg": False})
1718
return ard
1819

1920
@pytest.fixture
@@ -55,7 +56,7 @@ def test_add_lg_suffix_single_allele_default(self, allele_handler):
5556

5657
def test_add_lg_suffix_single_allele_ars(self, mock_ard):
5758
"""Test add_lg_suffix with single allele using ARS suffix"""
58-
mock_ard._config = {"ARS_as_lg": True}
59+
mock_ard.config = ARDConfig.from_dict({"ARS_as_lg": True})
5960
handler = AlleleHandler(mock_ard)
6061
result = handler.add_lg_suffix("A*01:01")
6162
assert result == "A*01:01ARS"
@@ -67,7 +68,7 @@ def test_add_lg_suffix_multiple_alleles(self, allele_handler):
6768

6869
def test_add_lg_suffix_multiple_alleles_ars(self, mock_ard):
6970
"""Test add_lg_suffix with multiple alleles using ARS suffix"""
70-
mock_ard._config = {"ARS_as_lg": True}
71+
mock_ard.config = ARDConfig.from_dict({"ARS_as_lg": True})
7172
handler = AlleleHandler(mock_ard)
7273
result = handler.add_lg_suffix("A*01:01/A*01:02")
7374
assert result == "A*01:01ARS/A*01:02ARS"

tests/unit/handlers/test_shortnull_handler.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pytest
44
from unittest.mock import Mock
55

6+
from pyard import ARDConfig
67
from pyard.handlers.shortnull_handler import ShortNullHandler
78

89

@@ -13,7 +14,7 @@ class TestShortNullHandler:
1314
def mock_ard(self):
1415
"""Create mock ARD instance"""
1516
ard = Mock()
16-
ard._config = {"reduce_shortnull": True}
17+
ard.config = ARDConfig.from_dict({"reduce_shortnull": True})
1718
ard.shortnulls = {"A*01:01N", "B*07:02N"}
1819
ard.is_mac.return_value = False
1920
return ard
@@ -35,7 +36,7 @@ def test_is_shortnull_valid_with_config_enabled(self, shortnull_handler):
3536

3637
def test_is_shortnull_valid_with_config_disabled(self, mock_ard):
3738
"""Test is_shortnull with valid short null but config disabled"""
38-
mock_ard._config["reduce_shortnull"] = False
39+
mock_ard.config = ARDConfig.from_dict({"reduce_shortnull": False})
3940
handler = ShortNullHandler(mock_ard)
4041

4142
result = handler.is_shortnull("A*01:01N")
@@ -95,7 +96,7 @@ def test_is_shortnull_combinations(
9596
self, mock_ard, allele, in_shortnulls, config_enabled, expected
9697
):
9798
"""Test is_shortnull with various combinations of conditions"""
98-
mock_ard._config["reduce_shortnull"] = config_enabled
99+
mock_ard.config = ARDConfig.from_dict({"reduce_shortnull": config_enabled})
99100
mock_ard.shortnulls = {allele} if in_shortnulls else set()
100101
handler = ShortNullHandler(mock_ard)
101102

0 commit comments

Comments
 (0)