Skip to content

Commit 32ddbad

Browse files
committed
pathlibs were being written!
What! I thought we fixed this -- and now they're not safe across Mac to Unix!
1 parent 90739ef commit 32ddbad

File tree

4 files changed

+64
-16
lines changed

4 files changed

+64
-16
lines changed

dist/dist.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
8080
Finish this before doing the next step, even though it looks like it could be done in parallel.
8181
82-
19a. Upload the new file to PyPI with "twine upload music21-9.3.0.tar.gz" [*]
82+
19a. Upload the tar.gz file to PyPI with "twine upload music21-9.3.0.tar.gz" [*]
8383
8484
19b. Do the same for the whl file (but not for the no-corpus file) [*]
8585
@@ -95,6 +95,10 @@
9595
username:__token__
9696
password:pypi-the_gibberish_generated_in_create_api_token_at_the_bottom_of_account_settings
9797
98+
The "password" is under "Account Settings" -> "API Token" -> "Options" -> "View Unique
99+
Identifier" -- the Copy button doesn't work at least on Mac.
100+
(or you can use the "password" field in the [pypi] section, but that's not recommended)')
101+
98102
FYI -- PyPI is apparently uninterested in having contributions from smaller projects
99103
which haven't been following their internal security discussions for years. All of this
100104
is super mysterious and not well documented. sigh. But they did mail out USB sticks to
-2.76 MB
Binary file not shown.

music21/metadata/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1396,7 +1396,8 @@ def fileFormat(self, value: str) -> None:
13961396
@property
13971397
def filePath(self) -> str|None:
13981398
'''
1399-
Get or set the file path that was parsed.
1399+
Get or set the file path that was parsed. This returns a string, not a Path object
1400+
that is deliberate for caching.
14001401
'''
14011402
return self._getSingularAttribute('filePath')
14021403

@@ -2481,7 +2482,7 @@ def __init__(self, **keywords):
24812482
self.pitchHighest = None
24822483
self.pitchLowest = None
24832484
self.quarterLength = None
2484-
self.sourcePath = ''
2485+
self.sourcePath: str = ''
24852486
self.tempoFirst = None
24862487
self.tempos = []
24872488
self.timeSignatureFirst = None

music21/metadata/bundles.py

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737

3838
if t.TYPE_CHECKING:
3939
from collections.abc import Iterable
40+
from music21.metadata import Metadata
4041

4142

4243
# -----------------------------------------------------------------------------
@@ -64,7 +65,8 @@ class MetadataEntry(prebase.ProtoM21Object):
6465
<music21.metadata.bundles.MetadataEntry 'bach_bwv66_6_mxl'>
6566
6667
The sourcePath of the metadata entry refers to the file path at which its
67-
score file is found:
68+
score file is found, but it is usually a relative path to the top of the corpus
69+
directory. It is a pathlib object.
6870
6971
>>> metadataEntry.sourcePath
7072
PosixPath('bach/bwv66.6.mxl')
@@ -85,21 +87,28 @@ class MetadataEntry(prebase.ProtoM21Object):
8587
# INITIALIZER #
8688

8789
def __init__(self,
88-
sourcePath=None,
89-
number=None,
90-
metadataPayload=None,
91-
corpusName=None,
90+
sourcePath: str|pathlib.Path = '',
91+
number: int|None = None,
92+
metadataPayload: Metadata|None = None,
93+
corpusName: str = '',
9294
):
93-
self._sourcePath = str(sourcePath)
95+
# only store strings internally; sourcePath is s
96+
self._sourcePath: str = str(sourcePath)
9497
self._number = number
9598
self._metadataPayload = metadataPayload
9699
self._corpusName = corpusName
97100

98101
# SPECIAL METHODS #
99-
100102
def __getnewargs__(self):
103+
'''
104+
This is all the information that is needed for pickling.
105+
Specifically do not include _corpusName.
106+
107+
Note do not pickle Pathlib objects, so make sure to do ._sourcePath
108+
not .sourcePath
109+
'''
101110
return (
102-
self.sourcePath,
111+
self._sourcePath,
103112
self.metadata,
104113
self.number,
105114
)
@@ -109,7 +118,8 @@ def _reprInternal(self):
109118

110119
def __fspath__(self):
111120
'''
112-
for Py3.6+ to allow MetadataEntries to be used where file paths are being employed
121+
Allows MetadataEntries to be used where file paths are being employed,
122+
so it can be used with opening and closing, etc.
113123
114124
Returns self.sourcePath() as a string
115125
@@ -138,8 +148,14 @@ def search(self, query=None, field=None, **keywords):
138148

139149
# PUBLIC PROPERTIES #
140150

151+
# we seem to be storing everything as a property so that the object
152+
# is immutable after creating.
153+
141154
@property
142155
def corpusPath(self):
156+
'''
157+
Returns the sourcePath as a string, with _number appended if it is not None.
158+
'''
143159
return MetadataBundle.corpusPathToKey(self.sourcePath, self.number)
144160

145161
@property
@@ -638,7 +654,7 @@ def corpus(self, newCorpus):
638654
self._corpus = common.wrapWeakref(newCorpus)
639655

640656
@property
641-
def filePath(self):
657+
def filePath(self) -> pathlib.Path | None:
642658
r'''
643659
The filesystem name of the cached metadata bundle, if the metadata
644660
bundle's name is not None.
@@ -829,7 +845,7 @@ def clear(self):
829845
self._metadataEntries.clear()
830846

831847
@staticmethod
832-
def corpusPathToKey(filePath, number=None):
848+
def corpusPathToKey(filePath: str|pathlib.Path, number: int|None = None):
833849
r'''
834850
Given a file path or corpus path, return the metadata key:
835851
@@ -841,6 +857,12 @@ def corpusPathToKey(filePath, number=None):
841857
>>> key = mb.corpusPathToKey('corelli/opus3no1/1grave.xml')
842858
>>> key.endswith('corelli_opus3no1_1grave_xml')
843859
True
860+
861+
Numbers are appended if given
862+
863+
>>> key = mb.corpusPathToKey('corelli/opus3no1/1grave.xml', number=3)
864+
>>> key.endswith('corelli_opus3no1_1grave_xml_3')
865+
True
844866
'''
845867
if isinstance(filePath, pathlib.Path):
846868
try:
@@ -866,6 +888,7 @@ def corpusPathToKey(filePath, number=None):
866888
corpusPath = corpusPath.replace(os.sep, '_')
867889

868890
corpusPath = corpusPath.replace('.', '_')
891+
869892
# append name to metadata path
870893
if number is not None:
871894
return f'{corpusPath}_{number}'
@@ -1315,18 +1338,38 @@ def write(self, filePath=None):
13151338
filePath = filePath or self.filePath
13161339
if self.filePath is not None:
13171340
filePath = self.filePath
1341+
13181342
environLocal.printDebug(['MetadataBundle: writing:', filePath])
13191343
storedCorpusClient = self._corpus # no weakrefs allowed
13201344
self._corpus = None
1321-
uncompressed = pickle.dumps(self, protocol=3)
1322-
# 3 is a safe protocol for some time to come.
1345+
1346+
# Protocol 5 is Python 3.8 and above.
1347+
uncompressed = pickle.dumps(self, protocol=5)
1348+
1349+
# # uncomment this and SafePickler when we next need to
1350+
# # figure out where Pathlib objects are sneaking into
1351+
# # the pickle -- we've had to rediagnose this too many times
1352+
# # to count.
1353+
# with open(filePath, 'wb') as f:
1354+
# SafePickler(f, protocol=5).dump(self)
1355+
#
1356+
# with open(filePath, 'rb') as f:
1357+
# uncompressed = f.read() # ridiculous...
13231358

13241359
with gzip.open(filePath, 'wb') as outFp:
13251360
outFp.write(uncompressed)
13261361
self._corpus = storedCorpusClient
13271362

13281363
return self
13291364

1365+
# class SafePickler(pickle.Pickler):
1366+
# def persistent_id(self, obj):
1367+
# if isinstance(obj, pathlib.Path):
1368+
# raise TypeError(f'Pickling pathlib.Path objects is not allowed! {obj}')
1369+
# else:
1370+
# print(obj)
1371+
# return None # Default behavior for other objects
1372+
13301373

13311374
_test_bundles: dict[str, MetadataBundle] = {}
13321375

0 commit comments

Comments
 (0)