Skip to content

Commit a9b1883

Browse files
authored
Fix a bug with using fields with special characters in records (#1031)
Fixes #1029
1 parent 7f1b75a commit a9b1883

File tree

6 files changed

+53
-18
lines changed

6 files changed

+53
-18
lines changed

.github/workflows/run-tests.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ jobs:
7777
with:
7878
name: code-coverage.py${{ matrix.version }}
7979
path: coverage/.coverage.py*
80+
include-hidden-files: true
81+
if-no-files-found: error
8082

8183
run-pypy-tests:
8284
runs-on: ${{matrix.os}}
@@ -126,6 +128,8 @@ jobs:
126128
with:
127129
name: code-coverage.pypy${{ matrix.version }}
128130
path: coverage/.coverage.py*
131+
include-hidden-files: true
132+
if-no-files-found: error
129133

130134
min-deps-test:
131135
# Attempt to run the TOX-ENV tests for the given OS and python

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
* Fix a bug where the compiler would always generate inline function definitions even if the `inline-functions` compiler option is disabled (#1023)
1010
* Fix a bug where `defrecord`/`deftype` constructors could not be used in the type's methods. (#1025)
1111
* Fix a bug where `keys` and `vals` would fail for records (#1030)
12+
* Fix a bug where operations on records created by `defrecord` failed for fields whose Python-safe names were mangled by the Python compiler (#1029)
1213

1314
## [v0.2.1]
1415
### Changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ include = ["README.md", "LICENSE"]
3131

3232
[tool.poetry.dependencies]
3333
python = "^3.8"
34-
attrs = ">=20.2.0"
34+
attrs = ">=22.2.0"
3535
immutables = ">=0.20,<1.0.0"
3636
prompt-toolkit = ">=3.0.0,<4.0.0"
3737
pyrsistent = ">=0.18.0,<1.0.0"

src/basilisp/core.lpy

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6675,6 +6675,16 @@
66756675

66766676
(prefer-method evolve basilisp.lang.interfaces/IRecord basilisp.lang.interfaces/IPersistentMap)
66776677

6678+
(defn -mangle-py-field
6679+
"Implementation detail of :lpy:fn:`defrecord` to support Python's managled field
6680+
names."
6681+
[type-sym field-name]
6682+
(if (and (.startswith field-name "__")
6683+
(not (.endswith field-name "__")))
6684+
(let [munged-type-name (.lstrip (munge type-sym) "_")]
6685+
(str "_" munged-type-name field-name))
6686+
field-name))
6687+
66786688
(defn ^:private validate-record-fields
66796689
"Validate that record fields do not contain any reserved entries, are not declared as
66806690
mutable, and do not declare any default values."
@@ -6779,12 +6789,10 @@
67796789
:map))))
67806790

67816791
new-recmap# (when (seq map#)
6782-
;; Python attrs does not allow the underscore prefixed
6783-
;; field name in the constructor
6784-
["recmap" (->> (mapcat identity map#)
6785-
(apply assoc ~'_recmap))])]
6792+
["_recmap" (->> (mapcat identity map#)
6793+
(apply assoc ~'_recmap))])]
67866794
(->> fields#
6787-
(map (fn [[k# v#]] [(name k#) v#]))
6795+
(map (fn [[k# v#]] [(munge k#) v#]))
67886796
(mapcat identity)
67896797
(concat new-recmap#)
67906798
(apply evolve ~this-gs))))
@@ -6846,25 +6854,21 @@
68466854
(contains? (.- new-rec# ~'_recmap) f#)
68476855
(recur r#
68486856
(->> (dissoc (.- new-rec# ~'_recmap) f#)
6849-
(evolve new-rec# "recmap")))
6857+
(evolve new-rec# "_recmap")))
68506858

68516859
:else
68526860
(recur r# new-rec#))))
68536861
(~'contains [~this-gs ~key-gs]
68546862
(or (~field-kw-set ~key-gs)
68556863
(contains? ~'_recmap ~key-gs)))
68566864
(~'entry [~this-gs ~key-gs]
6857-
(cond
6858-
(contains? ~field-kw-set ~key-gs)
6859-
(map-entry ~key-gs (python/getattr ~this-gs (munge ~key-gs)))
6860-
6861-
(contains? ~'_recmap ~key-gs)
6862-
(map-entry ~key-gs (get ~'_recmap ~key-gs))))
6865+
(when-let [val# (.val-at ~this-gs ~key-gs)]
6866+
(map-entry ~key-gs val#)))
68636867
(~'val-at [~this-gs ~key-gs ~'& args#]
68646868
(let [[default#] args#]
68656869
(cond
68666870
(contains? ~field-kw-set ~key-gs)
6867-
(python/getattr ~this-gs (munge ~key-gs))
6871+
(python/getattr ~this-gs (-mangle-py-field '~type-name (munge ~key-gs)))
68686872

68696873
(contains? ~'_recmap ~key-gs)
68706874
(get ~'_recmap ~key-gs default#))))
@@ -6891,12 +6895,16 @@
68916895
(~'_record_lrepr [~this-gs py-kwargs#]
68926896
(let [{print-meta :print_meta} (py->lisp py-kwargs#)
68936897

6894-
ns-name (name *ns*)
6895-
qual-name (.- ~type-name ~'__qualname__)]
6898+
;; Accessing this type name directly as `(.-__qualname__ this)`
6899+
;; can fail in cases where the type name is munged and starts
6900+
;; with a leading double underscore. Python apparently mangles
6901+
;; the name as it would for any object field prefixed with a
6902+
;; dunder.
6903+
qual-name (.- (python/type ~this-gs) ~'__qualname__)]
68966904
(cond->> (->> (mapcat identity ~this-gs)
68976905
(apply hash-map)
68986906
(repr)
6899-
(str "#" ns-name "." qual-name))
6907+
(str "#" ~(name *ns*) "." qual-name))
69006908
print-meta (str "^" (repr (meta ~this-gs)) " "))))
69016909

69026910
;; object

src/basilisp/lang/compiler/generator.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1468,7 +1468,12 @@ def _deftype_to_py_ast( # pylint: disable=too-many-locals
14681468
_tagged_assign(
14691469
target=ast.Name(id=safe_field, ctx=ast.Store()),
14701470
value=ast.Call(
1471-
func=_ATTRIB_FIELD_FN_NAME, args=[], keywords=attr_default_kws
1471+
func=_ATTRIB_FIELD_FN_NAME,
1472+
args=[],
1473+
keywords=[
1474+
*attr_default_kws,
1475+
ast.keyword(arg="alias", value=ast.Constant(safe_field)),
1476+
],
14721477
),
14731478
annotation=tag,
14741479
)

tests/basilisp/test_defrecord.lpy

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,20 @@
259259
"#tests.basilisp.test-defrecord.Circle{:name \"Kurt\" :radius 1}"}
260260
(str c1))))))
261261

262+
(defrecord ?Name [? x? ?x ?? ?x? ??x ?x- x?x])
263+
264+
(deftest defrecord-with-munged-fields
265+
(let [fields [:? :x? :?x :?? :?x? :??x :?x- :x?x]
266+
o (->?Name 1 2 3 4 5 6 7 8)]
267+
(is (= [1 2 3 4 5 6 7 8]
268+
(map (partial get o) fields)))
269+
(is (= [2 3 4 5 6 7 8 9]
270+
(map #(get (assoc o % (inc (get o %))) %) fields)))
271+
(is (= o (dissoc (assoc o :other-key 1000) :other-key)))
272+
(is (= (dissoc (assoc (zipmap fields (range 1 100)) :other-key 1000) :?x)
273+
(dissoc (assoc o :other-key 1000) :?x)))
274+
(is (= o (map->?Name (zipmap fields (range 1 100)))))))
275+
262276
(deftest defrecord-reader-form
263277
(testing "record without methods"
264278
(let [p (->Point 1 2 3)
@@ -287,6 +301,9 @@
287301
(read-string "#tests.basilisp.test-defrecord.Point(1 2 3)")))))
288302

289303
(deftest defrecord-field-restrictions
304+
(testing "attrs does not strip prefixes"
305+
(is (eval '(defrecord NonDuplicateFieldName [x -x]))))
306+
290307
(testing "reserved fields"
291308
(is (thrown? basilisp.lang.compiler/CompilerException
292309
(eval '(defrecord NewType [a b meta]))))

0 commit comments

Comments
 (0)