fonttools 4.60.2__cp311-cp311-win32.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fontTools/__init__.py +8 -0
- fontTools/__main__.py +35 -0
- fontTools/afmLib.py +439 -0
- fontTools/agl.py +5233 -0
- fontTools/annotations.py +30 -0
- fontTools/cffLib/CFF2ToCFF.py +258 -0
- fontTools/cffLib/CFFToCFF2.py +305 -0
- fontTools/cffLib/__init__.py +3694 -0
- fontTools/cffLib/specializer.py +927 -0
- fontTools/cffLib/transforms.py +495 -0
- fontTools/cffLib/width.py +210 -0
- fontTools/colorLib/__init__.py +0 -0
- fontTools/colorLib/builder.py +664 -0
- fontTools/colorLib/errors.py +2 -0
- fontTools/colorLib/geometry.py +143 -0
- fontTools/colorLib/table_builder.py +223 -0
- fontTools/colorLib/unbuilder.py +81 -0
- fontTools/config/__init__.py +90 -0
- fontTools/cu2qu/__init__.py +15 -0
- fontTools/cu2qu/__main__.py +6 -0
- fontTools/cu2qu/benchmark.py +54 -0
- fontTools/cu2qu/cli.py +198 -0
- fontTools/cu2qu/cu2qu.c +15817 -0
- fontTools/cu2qu/cu2qu.cp311-win32.pyd +0 -0
- fontTools/cu2qu/cu2qu.py +563 -0
- fontTools/cu2qu/errors.py +77 -0
- fontTools/cu2qu/ufo.py +363 -0
- fontTools/designspaceLib/__init__.py +3343 -0
- fontTools/designspaceLib/__main__.py +6 -0
- fontTools/designspaceLib/split.py +475 -0
- fontTools/designspaceLib/statNames.py +260 -0
- fontTools/designspaceLib/types.py +147 -0
- fontTools/encodings/MacRoman.py +258 -0
- fontTools/encodings/StandardEncoding.py +258 -0
- fontTools/encodings/__init__.py +1 -0
- fontTools/encodings/codecs.py +135 -0
- fontTools/feaLib/__init__.py +4 -0
- fontTools/feaLib/__main__.py +78 -0
- fontTools/feaLib/ast.py +2143 -0
- fontTools/feaLib/builder.py +1814 -0
- fontTools/feaLib/error.py +22 -0
- fontTools/feaLib/lexer.c +17029 -0
- fontTools/feaLib/lexer.cp311-win32.pyd +0 -0
- fontTools/feaLib/lexer.py +287 -0
- fontTools/feaLib/location.py +12 -0
- fontTools/feaLib/lookupDebugInfo.py +12 -0
- fontTools/feaLib/parser.py +2394 -0
- fontTools/feaLib/variableScalar.py +118 -0
- fontTools/fontBuilder.py +1014 -0
- fontTools/help.py +36 -0
- fontTools/merge/__init__.py +248 -0
- fontTools/merge/__main__.py +6 -0
- fontTools/merge/base.py +81 -0
- fontTools/merge/cmap.py +173 -0
- fontTools/merge/layout.py +526 -0
- fontTools/merge/options.py +85 -0
- fontTools/merge/tables.py +352 -0
- fontTools/merge/unicode.py +78 -0
- fontTools/merge/util.py +143 -0
- fontTools/misc/__init__.py +1 -0
- fontTools/misc/arrayTools.py +424 -0
- fontTools/misc/bezierTools.c +39731 -0
- fontTools/misc/bezierTools.cp311-win32.pyd +0 -0
- fontTools/misc/bezierTools.py +1500 -0
- fontTools/misc/classifyTools.py +170 -0
- fontTools/misc/cliTools.py +53 -0
- fontTools/misc/configTools.py +349 -0
- fontTools/misc/cython.py +27 -0
- fontTools/misc/dictTools.py +83 -0
- fontTools/misc/eexec.py +119 -0
- fontTools/misc/encodingTools.py +72 -0
- fontTools/misc/enumTools.py +23 -0
- fontTools/misc/etree.py +456 -0
- fontTools/misc/filenames.py +245 -0
- fontTools/misc/filesystem/__init__.py +68 -0
- fontTools/misc/filesystem/_base.py +134 -0
- fontTools/misc/filesystem/_copy.py +45 -0
- fontTools/misc/filesystem/_errors.py +54 -0
- fontTools/misc/filesystem/_info.py +75 -0
- fontTools/misc/filesystem/_osfs.py +164 -0
- fontTools/misc/filesystem/_path.py +67 -0
- fontTools/misc/filesystem/_subfs.py +92 -0
- fontTools/misc/filesystem/_tempfs.py +34 -0
- fontTools/misc/filesystem/_tools.py +34 -0
- fontTools/misc/filesystem/_walk.py +55 -0
- fontTools/misc/filesystem/_zipfs.py +204 -0
- fontTools/misc/fixedTools.py +253 -0
- fontTools/misc/intTools.py +25 -0
- fontTools/misc/iterTools.py +12 -0
- fontTools/misc/lazyTools.py +42 -0
- fontTools/misc/loggingTools.py +543 -0
- fontTools/misc/macCreatorType.py +56 -0
- fontTools/misc/macRes.py +261 -0
- fontTools/misc/plistlib/__init__.py +681 -0
- fontTools/misc/plistlib/py.typed +0 -0
- fontTools/misc/psCharStrings.py +1511 -0
- fontTools/misc/psLib.py +398 -0
- fontTools/misc/psOperators.py +572 -0
- fontTools/misc/py23.py +96 -0
- fontTools/misc/roundTools.py +110 -0
- fontTools/misc/sstruct.py +227 -0
- fontTools/misc/symfont.py +242 -0
- fontTools/misc/testTools.py +233 -0
- fontTools/misc/textTools.py +156 -0
- fontTools/misc/timeTools.py +88 -0
- fontTools/misc/transform.py +516 -0
- fontTools/misc/treeTools.py +45 -0
- fontTools/misc/vector.py +147 -0
- fontTools/misc/visitor.py +158 -0
- fontTools/misc/xmlReader.py +188 -0
- fontTools/misc/xmlWriter.py +231 -0
- fontTools/mtiLib/__init__.py +1400 -0
- fontTools/mtiLib/__main__.py +5 -0
- fontTools/otlLib/__init__.py +1 -0
- fontTools/otlLib/builder.py +3465 -0
- fontTools/otlLib/error.py +11 -0
- fontTools/otlLib/maxContextCalc.py +96 -0
- fontTools/otlLib/optimize/__init__.py +53 -0
- fontTools/otlLib/optimize/__main__.py +6 -0
- fontTools/otlLib/optimize/gpos.py +439 -0
- fontTools/pens/__init__.py +1 -0
- fontTools/pens/areaPen.py +52 -0
- fontTools/pens/basePen.py +475 -0
- fontTools/pens/boundsPen.py +98 -0
- fontTools/pens/cairoPen.py +26 -0
- fontTools/pens/cocoaPen.py +26 -0
- fontTools/pens/cu2quPen.py +325 -0
- fontTools/pens/explicitClosingLinePen.py +101 -0
- fontTools/pens/filterPen.py +433 -0
- fontTools/pens/freetypePen.py +462 -0
- fontTools/pens/hashPointPen.py +89 -0
- fontTools/pens/momentsPen.c +13378 -0
- fontTools/pens/momentsPen.cp311-win32.pyd +0 -0
- fontTools/pens/momentsPen.py +879 -0
- fontTools/pens/perimeterPen.py +69 -0
- fontTools/pens/pointInsidePen.py +192 -0
- fontTools/pens/pointPen.py +643 -0
- fontTools/pens/qtPen.py +29 -0
- fontTools/pens/qu2cuPen.py +105 -0
- fontTools/pens/quartzPen.py +43 -0
- fontTools/pens/recordingPen.py +335 -0
- fontTools/pens/reportLabPen.py +79 -0
- fontTools/pens/reverseContourPen.py +96 -0
- fontTools/pens/roundingPen.py +130 -0
- fontTools/pens/statisticsPen.py +312 -0
- fontTools/pens/svgPathPen.py +310 -0
- fontTools/pens/t2CharStringPen.py +88 -0
- fontTools/pens/teePen.py +55 -0
- fontTools/pens/transformPen.py +115 -0
- fontTools/pens/ttGlyphPen.py +335 -0
- fontTools/pens/wxPen.py +29 -0
- fontTools/qu2cu/__init__.py +15 -0
- fontTools/qu2cu/__main__.py +7 -0
- fontTools/qu2cu/benchmark.py +56 -0
- fontTools/qu2cu/cli.py +125 -0
- fontTools/qu2cu/qu2cu.c +16682 -0
- fontTools/qu2cu/qu2cu.cp311-win32.pyd +0 -0
- fontTools/qu2cu/qu2cu.py +405 -0
- fontTools/subset/__init__.py +4096 -0
- fontTools/subset/__main__.py +6 -0
- fontTools/subset/cff.py +184 -0
- fontTools/subset/svg.py +253 -0
- fontTools/subset/util.py +25 -0
- fontTools/svgLib/__init__.py +3 -0
- fontTools/svgLib/path/__init__.py +65 -0
- fontTools/svgLib/path/arc.py +154 -0
- fontTools/svgLib/path/parser.py +322 -0
- fontTools/svgLib/path/shapes.py +183 -0
- fontTools/t1Lib/__init__.py +648 -0
- fontTools/tfmLib.py +460 -0
- fontTools/ttLib/__init__.py +30 -0
- fontTools/ttLib/__main__.py +148 -0
- fontTools/ttLib/macUtils.py +54 -0
- fontTools/ttLib/removeOverlaps.py +395 -0
- fontTools/ttLib/reorderGlyphs.py +285 -0
- fontTools/ttLib/scaleUpem.py +436 -0
- fontTools/ttLib/sfnt.py +661 -0
- fontTools/ttLib/standardGlyphOrder.py +271 -0
- fontTools/ttLib/tables/B_A_S_E_.py +14 -0
- fontTools/ttLib/tables/BitmapGlyphMetrics.py +64 -0
- fontTools/ttLib/tables/C_B_D_T_.py +113 -0
- fontTools/ttLib/tables/C_B_L_C_.py +19 -0
- fontTools/ttLib/tables/C_F_F_.py +61 -0
- fontTools/ttLib/tables/C_F_F__2.py +26 -0
- fontTools/ttLib/tables/C_O_L_R_.py +165 -0
- fontTools/ttLib/tables/C_P_A_L_.py +305 -0
- fontTools/ttLib/tables/D_S_I_G_.py +158 -0
- fontTools/ttLib/tables/D__e_b_g.py +35 -0
- fontTools/ttLib/tables/DefaultTable.py +49 -0
- fontTools/ttLib/tables/E_B_D_T_.py +835 -0
- fontTools/ttLib/tables/E_B_L_C_.py +718 -0
- fontTools/ttLib/tables/F_F_T_M_.py +52 -0
- fontTools/ttLib/tables/F__e_a_t.py +149 -0
- fontTools/ttLib/tables/G_D_E_F_.py +13 -0
- fontTools/ttLib/tables/G_M_A_P_.py +148 -0
- fontTools/ttLib/tables/G_P_K_G_.py +133 -0
- fontTools/ttLib/tables/G_P_O_S_.py +14 -0
- fontTools/ttLib/tables/G_S_U_B_.py +13 -0
- fontTools/ttLib/tables/G_V_A_R_.py +5 -0
- fontTools/ttLib/tables/G__l_a_t.py +235 -0
- fontTools/ttLib/tables/G__l_o_c.py +85 -0
- fontTools/ttLib/tables/H_V_A_R_.py +13 -0
- fontTools/ttLib/tables/J_S_T_F_.py +13 -0
- fontTools/ttLib/tables/L_T_S_H_.py +58 -0
- fontTools/ttLib/tables/M_A_T_H_.py +13 -0
- fontTools/ttLib/tables/M_E_T_A_.py +352 -0
- fontTools/ttLib/tables/M_V_A_R_.py +13 -0
- fontTools/ttLib/tables/O_S_2f_2.py +752 -0
- fontTools/ttLib/tables/S_I_N_G_.py +99 -0
- fontTools/ttLib/tables/S_T_A_T_.py +15 -0
- fontTools/ttLib/tables/S_V_G_.py +223 -0
- fontTools/ttLib/tables/S__i_l_f.py +1040 -0
- fontTools/ttLib/tables/S__i_l_l.py +92 -0
- fontTools/ttLib/tables/T_S_I_B_.py +13 -0
- fontTools/ttLib/tables/T_S_I_C_.py +14 -0
- fontTools/ttLib/tables/T_S_I_D_.py +13 -0
- fontTools/ttLib/tables/T_S_I_J_.py +13 -0
- fontTools/ttLib/tables/T_S_I_P_.py +13 -0
- fontTools/ttLib/tables/T_S_I_S_.py +13 -0
- fontTools/ttLib/tables/T_S_I_V_.py +26 -0
- fontTools/ttLib/tables/T_S_I__0.py +70 -0
- fontTools/ttLib/tables/T_S_I__1.py +163 -0
- fontTools/ttLib/tables/T_S_I__2.py +17 -0
- fontTools/ttLib/tables/T_S_I__3.py +22 -0
- fontTools/ttLib/tables/T_S_I__5.py +60 -0
- fontTools/ttLib/tables/T_T_F_A_.py +14 -0
- fontTools/ttLib/tables/TupleVariation.py +884 -0
- fontTools/ttLib/tables/V_A_R_C_.py +12 -0
- fontTools/ttLib/tables/V_D_M_X_.py +249 -0
- fontTools/ttLib/tables/V_O_R_G_.py +165 -0
- fontTools/ttLib/tables/V_V_A_R_.py +13 -0
- fontTools/ttLib/tables/__init__.py +98 -0
- fontTools/ttLib/tables/_a_n_k_r.py +15 -0
- fontTools/ttLib/tables/_a_v_a_r.py +193 -0
- fontTools/ttLib/tables/_b_s_l_n.py +15 -0
- fontTools/ttLib/tables/_c_i_d_g.py +24 -0
- fontTools/ttLib/tables/_c_m_a_p.py +1591 -0
- fontTools/ttLib/tables/_c_v_a_r.py +94 -0
- fontTools/ttLib/tables/_c_v_t.py +56 -0
- fontTools/ttLib/tables/_f_e_a_t.py +15 -0
- fontTools/ttLib/tables/_f_p_g_m.py +62 -0
- fontTools/ttLib/tables/_f_v_a_r.py +261 -0
- fontTools/ttLib/tables/_g_a_s_p.py +63 -0
- fontTools/ttLib/tables/_g_c_i_d.py +13 -0
- fontTools/ttLib/tables/_g_l_y_f.py +2311 -0
- fontTools/ttLib/tables/_g_v_a_r.py +340 -0
- fontTools/ttLib/tables/_h_d_m_x.py +127 -0
- fontTools/ttLib/tables/_h_e_a_d.py +130 -0
- fontTools/ttLib/tables/_h_h_e_a.py +147 -0
- fontTools/ttLib/tables/_h_m_t_x.py +164 -0
- fontTools/ttLib/tables/_k_e_r_n.py +289 -0
- fontTools/ttLib/tables/_l_c_a_r.py +13 -0
- fontTools/ttLib/tables/_l_o_c_a.py +70 -0
- fontTools/ttLib/tables/_l_t_a_g.py +72 -0
- fontTools/ttLib/tables/_m_a_x_p.py +147 -0
- fontTools/ttLib/tables/_m_e_t_a.py +112 -0
- fontTools/ttLib/tables/_m_o_r_t.py +14 -0
- fontTools/ttLib/tables/_m_o_r_x.py +15 -0
- fontTools/ttLib/tables/_n_a_m_e.py +1242 -0
- fontTools/ttLib/tables/_o_p_b_d.py +14 -0
- fontTools/ttLib/tables/_p_o_s_t.py +319 -0
- fontTools/ttLib/tables/_p_r_e_p.py +16 -0
- fontTools/ttLib/tables/_p_r_o_p.py +12 -0
- fontTools/ttLib/tables/_s_b_i_x.py +129 -0
- fontTools/ttLib/tables/_t_r_a_k.py +332 -0
- fontTools/ttLib/tables/_v_h_e_a.py +139 -0
- fontTools/ttLib/tables/_v_m_t_x.py +19 -0
- fontTools/ttLib/tables/asciiTable.py +20 -0
- fontTools/ttLib/tables/grUtils.py +92 -0
- fontTools/ttLib/tables/otBase.py +1458 -0
- fontTools/ttLib/tables/otConverters.py +2068 -0
- fontTools/ttLib/tables/otData.py +6400 -0
- fontTools/ttLib/tables/otTables.py +2703 -0
- fontTools/ttLib/tables/otTraverse.py +163 -0
- fontTools/ttLib/tables/sbixGlyph.py +149 -0
- fontTools/ttLib/tables/sbixStrike.py +177 -0
- fontTools/ttLib/tables/table_API_readme.txt +91 -0
- fontTools/ttLib/tables/ttProgram.py +594 -0
- fontTools/ttLib/ttCollection.py +125 -0
- fontTools/ttLib/ttFont.py +1148 -0
- fontTools/ttLib/ttGlyphSet.py +490 -0
- fontTools/ttLib/ttVisitor.py +32 -0
- fontTools/ttLib/woff2.py +1680 -0
- fontTools/ttx.py +479 -0
- fontTools/ufoLib/__init__.py +2575 -0
- fontTools/ufoLib/converters.py +407 -0
- fontTools/ufoLib/errors.py +30 -0
- fontTools/ufoLib/etree.py +6 -0
- fontTools/ufoLib/filenames.py +356 -0
- fontTools/ufoLib/glifLib.py +2120 -0
- fontTools/ufoLib/kerning.py +141 -0
- fontTools/ufoLib/plistlib.py +47 -0
- fontTools/ufoLib/pointPen.py +6 -0
- fontTools/ufoLib/utils.py +107 -0
- fontTools/ufoLib/validators.py +1208 -0
- fontTools/unicode.py +50 -0
- fontTools/unicodedata/Blocks.py +817 -0
- fontTools/unicodedata/Mirrored.py +446 -0
- fontTools/unicodedata/OTTags.py +50 -0
- fontTools/unicodedata/ScriptExtensions.py +832 -0
- fontTools/unicodedata/Scripts.py +3639 -0
- fontTools/unicodedata/__init__.py +306 -0
- fontTools/varLib/__init__.py +1600 -0
- fontTools/varLib/__main__.py +6 -0
- fontTools/varLib/avar/__init__.py +0 -0
- fontTools/varLib/avar/__main__.py +72 -0
- fontTools/varLib/avar/build.py +79 -0
- fontTools/varLib/avar/map.py +108 -0
- fontTools/varLib/avar/plan.py +1004 -0
- fontTools/varLib/avar/unbuild.py +271 -0
- fontTools/varLib/avarPlanner.py +8 -0
- fontTools/varLib/builder.py +215 -0
- fontTools/varLib/cff.py +631 -0
- fontTools/varLib/errors.py +219 -0
- fontTools/varLib/featureVars.py +703 -0
- fontTools/varLib/hvar.py +113 -0
- fontTools/varLib/instancer/__init__.py +2052 -0
- fontTools/varLib/instancer/__main__.py +5 -0
- fontTools/varLib/instancer/featureVars.py +190 -0
- fontTools/varLib/instancer/names.py +388 -0
- fontTools/varLib/instancer/solver.py +309 -0
- fontTools/varLib/interpolatable.py +1209 -0
- fontTools/varLib/interpolatableHelpers.py +399 -0
- fontTools/varLib/interpolatablePlot.py +1269 -0
- fontTools/varLib/interpolatableTestContourOrder.py +82 -0
- fontTools/varLib/interpolatableTestStartingPoint.py +107 -0
- fontTools/varLib/interpolate_layout.py +124 -0
- fontTools/varLib/iup.c +19815 -0
- fontTools/varLib/iup.cp311-win32.pyd +0 -0
- fontTools/varLib/iup.py +490 -0
- fontTools/varLib/merger.py +1717 -0
- fontTools/varLib/models.py +642 -0
- fontTools/varLib/multiVarStore.py +253 -0
- fontTools/varLib/mutator.py +529 -0
- fontTools/varLib/mvar.py +40 -0
- fontTools/varLib/plot.py +238 -0
- fontTools/varLib/stat.py +149 -0
- fontTools/varLib/varStore.py +739 -0
- fontTools/voltLib/__init__.py +5 -0
- fontTools/voltLib/__main__.py +206 -0
- fontTools/voltLib/ast.py +452 -0
- fontTools/voltLib/error.py +12 -0
- fontTools/voltLib/lexer.py +99 -0
- fontTools/voltLib/parser.py +664 -0
- fontTools/voltLib/voltToFea.py +911 -0
- fonttools-4.60.2.data/data/share/man/man1/ttx.1 +225 -0
- fonttools-4.60.2.dist-info/METADATA +2250 -0
- fonttools-4.60.2.dist-info/RECORD +353 -0
- fonttools-4.60.2.dist-info/WHEEL +5 -0
- fonttools-4.60.2.dist-info/entry_points.txt +5 -0
- fonttools-4.60.2.dist-info/licenses/LICENSE +21 -0
- fonttools-4.60.2.dist-info/licenses/LICENSE.external +388 -0
- fonttools-4.60.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module implements the algorithm for converting between a "user name" -
|
|
3
|
+
something that a user can choose arbitrarily inside a font editor - and a file
|
|
4
|
+
name suitable for use in a wide range of operating systems and filesystems.
|
|
5
|
+
|
|
6
|
+
The `UFO 3 specification <http://unifiedfontobject.org/versions/ufo3/conventions/>`_
|
|
7
|
+
provides an example of an algorithm for such conversion, which avoids illegal
|
|
8
|
+
characters, reserved file names, ambiguity between upper- and lower-case
|
|
9
|
+
characters, and clashes with existing files.
|
|
10
|
+
|
|
11
|
+
This code was originally copied from
|
|
12
|
+
`ufoLib <https://github.com/unified-font-object/ufoLib/blob/8747da7/Lib/ufoLib/filenames.py>`_
|
|
13
|
+
by Tal Leming and is copyright (c) 2005-2016, The RoboFab Developers:
|
|
14
|
+
|
|
15
|
+
- Erik van Blokland
|
|
16
|
+
- Tal Leming
|
|
17
|
+
- Just van Rossum
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
illegalCharacters = r"\" * + / : < > ? [ \ ] | \0".split(" ")
|
|
21
|
+
illegalCharacters += [chr(i) for i in range(1, 32)]
|
|
22
|
+
illegalCharacters += [chr(0x7F)]
|
|
23
|
+
reservedFileNames = "CON PRN AUX CLOCK$ NUL A:-Z: COM1".lower().split(" ")
|
|
24
|
+
reservedFileNames += "LPT1 LPT2 LPT3 COM2 COM3 COM4".lower().split(" ")
|
|
25
|
+
maxFileNameLength = 255
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class NameTranslationError(Exception):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def userNameToFileName(userName, existing=[], prefix="", suffix=""):
|
|
33
|
+
"""Converts from a user name to a file name.
|
|
34
|
+
|
|
35
|
+
Takes care to avoid illegal characters, reserved file names, ambiguity between
|
|
36
|
+
upper- and lower-case characters, and clashes with existing files.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
userName (str): The input file name.
|
|
40
|
+
existing: A case-insensitive list of all existing file names.
|
|
41
|
+
prefix: Prefix to be prepended to the file name.
|
|
42
|
+
suffix: Suffix to be appended to the file name.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
A suitable filename.
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
NameTranslationError: If no suitable name could be generated.
|
|
49
|
+
|
|
50
|
+
Examples::
|
|
51
|
+
|
|
52
|
+
>>> userNameToFileName("a") == "a"
|
|
53
|
+
True
|
|
54
|
+
>>> userNameToFileName("A") == "A_"
|
|
55
|
+
True
|
|
56
|
+
>>> userNameToFileName("AE") == "A_E_"
|
|
57
|
+
True
|
|
58
|
+
>>> userNameToFileName("Ae") == "A_e"
|
|
59
|
+
True
|
|
60
|
+
>>> userNameToFileName("ae") == "ae"
|
|
61
|
+
True
|
|
62
|
+
>>> userNameToFileName("aE") == "aE_"
|
|
63
|
+
True
|
|
64
|
+
>>> userNameToFileName("a.alt") == "a.alt"
|
|
65
|
+
True
|
|
66
|
+
>>> userNameToFileName("A.alt") == "A_.alt"
|
|
67
|
+
True
|
|
68
|
+
>>> userNameToFileName("A.Alt") == "A_.A_lt"
|
|
69
|
+
True
|
|
70
|
+
>>> userNameToFileName("A.aLt") == "A_.aL_t"
|
|
71
|
+
True
|
|
72
|
+
>>> userNameToFileName(u"A.alT") == "A_.alT_"
|
|
73
|
+
True
|
|
74
|
+
>>> userNameToFileName("T_H") == "T__H_"
|
|
75
|
+
True
|
|
76
|
+
>>> userNameToFileName("T_h") == "T__h"
|
|
77
|
+
True
|
|
78
|
+
>>> userNameToFileName("t_h") == "t_h"
|
|
79
|
+
True
|
|
80
|
+
>>> userNameToFileName("F_F_I") == "F__F__I_"
|
|
81
|
+
True
|
|
82
|
+
>>> userNameToFileName("f_f_i") == "f_f_i"
|
|
83
|
+
True
|
|
84
|
+
>>> userNameToFileName("Aacute_V.swash") == "A_acute_V_.swash"
|
|
85
|
+
True
|
|
86
|
+
>>> userNameToFileName(".notdef") == "_notdef"
|
|
87
|
+
True
|
|
88
|
+
>>> userNameToFileName("con") == "_con"
|
|
89
|
+
True
|
|
90
|
+
>>> userNameToFileName("CON") == "C_O_N_"
|
|
91
|
+
True
|
|
92
|
+
>>> userNameToFileName("con.alt") == "_con.alt"
|
|
93
|
+
True
|
|
94
|
+
>>> userNameToFileName("alt.con") == "alt._con"
|
|
95
|
+
True
|
|
96
|
+
"""
|
|
97
|
+
# the incoming name must be a str
|
|
98
|
+
if not isinstance(userName, str):
|
|
99
|
+
raise ValueError("The value for userName must be a string.")
|
|
100
|
+
# establish the prefix and suffix lengths
|
|
101
|
+
prefixLength = len(prefix)
|
|
102
|
+
suffixLength = len(suffix)
|
|
103
|
+
# replace an initial period with an _
|
|
104
|
+
# if no prefix is to be added
|
|
105
|
+
if not prefix and userName[0] == ".":
|
|
106
|
+
userName = "_" + userName[1:]
|
|
107
|
+
# filter the user name
|
|
108
|
+
filteredUserName = []
|
|
109
|
+
for character in userName:
|
|
110
|
+
# replace illegal characters with _
|
|
111
|
+
if character in illegalCharacters:
|
|
112
|
+
character = "_"
|
|
113
|
+
# add _ to all non-lower characters
|
|
114
|
+
elif character != character.lower():
|
|
115
|
+
character += "_"
|
|
116
|
+
filteredUserName.append(character)
|
|
117
|
+
userName = "".join(filteredUserName)
|
|
118
|
+
# clip to 255
|
|
119
|
+
sliceLength = maxFileNameLength - prefixLength - suffixLength
|
|
120
|
+
userName = userName[:sliceLength]
|
|
121
|
+
# test for illegal files names
|
|
122
|
+
parts = []
|
|
123
|
+
for part in userName.split("."):
|
|
124
|
+
if part.lower() in reservedFileNames:
|
|
125
|
+
part = "_" + part
|
|
126
|
+
parts.append(part)
|
|
127
|
+
userName = ".".join(parts)
|
|
128
|
+
# test for clash
|
|
129
|
+
fullName = prefix + userName + suffix
|
|
130
|
+
if fullName.lower() in existing:
|
|
131
|
+
fullName = handleClash1(userName, existing, prefix, suffix)
|
|
132
|
+
# finished
|
|
133
|
+
return fullName
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def handleClash1(userName, existing=[], prefix="", suffix=""):
|
|
137
|
+
"""
|
|
138
|
+
existing should be a case-insensitive list
|
|
139
|
+
of all existing file names.
|
|
140
|
+
|
|
141
|
+
>>> prefix = ("0" * 5) + "."
|
|
142
|
+
>>> suffix = "." + ("0" * 10)
|
|
143
|
+
>>> existing = ["a" * 5]
|
|
144
|
+
|
|
145
|
+
>>> e = list(existing)
|
|
146
|
+
>>> handleClash1(userName="A" * 5, existing=e,
|
|
147
|
+
... prefix=prefix, suffix=suffix) == (
|
|
148
|
+
... '00000.AAAAA000000000000001.0000000000')
|
|
149
|
+
True
|
|
150
|
+
|
|
151
|
+
>>> e = list(existing)
|
|
152
|
+
>>> e.append(prefix + "aaaaa" + "1".zfill(15) + suffix)
|
|
153
|
+
>>> handleClash1(userName="A" * 5, existing=e,
|
|
154
|
+
... prefix=prefix, suffix=suffix) == (
|
|
155
|
+
... '00000.AAAAA000000000000002.0000000000')
|
|
156
|
+
True
|
|
157
|
+
|
|
158
|
+
>>> e = list(existing)
|
|
159
|
+
>>> e.append(prefix + "AAAAA" + "2".zfill(15) + suffix)
|
|
160
|
+
>>> handleClash1(userName="A" * 5, existing=e,
|
|
161
|
+
... prefix=prefix, suffix=suffix) == (
|
|
162
|
+
... '00000.AAAAA000000000000001.0000000000')
|
|
163
|
+
True
|
|
164
|
+
"""
|
|
165
|
+
# if the prefix length + user name length + suffix length + 15 is at
|
|
166
|
+
# or past the maximum length, silce 15 characters off of the user name
|
|
167
|
+
prefixLength = len(prefix)
|
|
168
|
+
suffixLength = len(suffix)
|
|
169
|
+
if prefixLength + len(userName) + suffixLength + 15 > maxFileNameLength:
|
|
170
|
+
l = prefixLength + len(userName) + suffixLength + 15
|
|
171
|
+
sliceLength = maxFileNameLength - l
|
|
172
|
+
userName = userName[:sliceLength]
|
|
173
|
+
finalName = None
|
|
174
|
+
# try to add numbers to create a unique name
|
|
175
|
+
counter = 1
|
|
176
|
+
while finalName is None:
|
|
177
|
+
name = userName + str(counter).zfill(15)
|
|
178
|
+
fullName = prefix + name + suffix
|
|
179
|
+
if fullName.lower() not in existing:
|
|
180
|
+
finalName = fullName
|
|
181
|
+
break
|
|
182
|
+
else:
|
|
183
|
+
counter += 1
|
|
184
|
+
if counter >= 999999999999999:
|
|
185
|
+
break
|
|
186
|
+
# if there is a clash, go to the next fallback
|
|
187
|
+
if finalName is None:
|
|
188
|
+
finalName = handleClash2(existing, prefix, suffix)
|
|
189
|
+
# finished
|
|
190
|
+
return finalName
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def handleClash2(existing=[], prefix="", suffix=""):
|
|
194
|
+
"""
|
|
195
|
+
existing should be a case-insensitive list
|
|
196
|
+
of all existing file names.
|
|
197
|
+
|
|
198
|
+
>>> prefix = ("0" * 5) + "."
|
|
199
|
+
>>> suffix = "." + ("0" * 10)
|
|
200
|
+
>>> existing = [prefix + str(i) + suffix for i in range(100)]
|
|
201
|
+
|
|
202
|
+
>>> e = list(existing)
|
|
203
|
+
>>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == (
|
|
204
|
+
... '00000.100.0000000000')
|
|
205
|
+
True
|
|
206
|
+
|
|
207
|
+
>>> e = list(existing)
|
|
208
|
+
>>> e.remove(prefix + "1" + suffix)
|
|
209
|
+
>>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == (
|
|
210
|
+
... '00000.1.0000000000')
|
|
211
|
+
True
|
|
212
|
+
|
|
213
|
+
>>> e = list(existing)
|
|
214
|
+
>>> e.remove(prefix + "2" + suffix)
|
|
215
|
+
>>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == (
|
|
216
|
+
... '00000.2.0000000000')
|
|
217
|
+
True
|
|
218
|
+
"""
|
|
219
|
+
# calculate the longest possible string
|
|
220
|
+
maxLength = maxFileNameLength - len(prefix) - len(suffix)
|
|
221
|
+
maxValue = int("9" * maxLength)
|
|
222
|
+
# try to find a number
|
|
223
|
+
finalName = None
|
|
224
|
+
counter = 1
|
|
225
|
+
while finalName is None:
|
|
226
|
+
fullName = prefix + str(counter) + suffix
|
|
227
|
+
if fullName.lower() not in existing:
|
|
228
|
+
finalName = fullName
|
|
229
|
+
break
|
|
230
|
+
else:
|
|
231
|
+
counter += 1
|
|
232
|
+
if counter >= maxValue:
|
|
233
|
+
break
|
|
234
|
+
# raise an error if nothing has been found
|
|
235
|
+
if finalName is None:
|
|
236
|
+
raise NameTranslationError("No unique name could be found.")
|
|
237
|
+
# finished
|
|
238
|
+
return finalName
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
if __name__ == "__main__":
|
|
242
|
+
import doctest
|
|
243
|
+
import sys
|
|
244
|
+
|
|
245
|
+
sys.exit(doctest.testmod().failed)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Minimal, stdlib-only replacement for [`pyfilesystem2`][1] API for use by `fontTools.ufoLib`.
|
|
2
|
+
|
|
3
|
+
This package is a partial reimplementation of the `fs` package by Will McGugan, used under the
|
|
4
|
+
MIT license. See LICENSE.external for details.
|
|
5
|
+
|
|
6
|
+
Note this only exports a **subset** of the `pyfilesystem2` API, in particular the modules,
|
|
7
|
+
classes and functions that are currently used directly by `fontTools.ufoLib`.
|
|
8
|
+
|
|
9
|
+
It opportunistically tries to import the relevant modules from the upstream `fs` package
|
|
10
|
+
when this is available. Otherwise it falls back to the replacement modules within this package.
|
|
11
|
+
|
|
12
|
+
As of version 4.59.0, the `fonttools[ufo]` extra no longer requires the `fs` package, thus
|
|
13
|
+
this `fontTools.misc.filesystem` package is used by default.
|
|
14
|
+
|
|
15
|
+
Client code can either replace `import fs` with `from fontTools.misc import filesystem as fs`
|
|
16
|
+
if that happens to work (no guarantee), or they can continue to use `fs` but they will have
|
|
17
|
+
to specify it as an explicit dependency of their project.
|
|
18
|
+
|
|
19
|
+
[1]: https://github.com/PyFilesystem/pyfilesystem2
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
__import__("fs")
|
|
26
|
+
except ImportError:
|
|
27
|
+
from . import _base as base
|
|
28
|
+
from . import _copy as copy
|
|
29
|
+
from . import _errors as errors
|
|
30
|
+
from . import _info as info
|
|
31
|
+
from . import _osfs as osfs
|
|
32
|
+
from . import _path as path
|
|
33
|
+
from . import _subfs as subfs
|
|
34
|
+
from . import _tempfs as tempfs
|
|
35
|
+
from . import _tools as tools
|
|
36
|
+
from . import _walk as walk
|
|
37
|
+
from . import _zipfs as zipfs
|
|
38
|
+
|
|
39
|
+
_haveFS = False
|
|
40
|
+
else:
|
|
41
|
+
import fs.base as base
|
|
42
|
+
import fs.copy as copy
|
|
43
|
+
import fs.errors as errors
|
|
44
|
+
import fs.info as info
|
|
45
|
+
import fs.osfs as osfs
|
|
46
|
+
import fs.path as path
|
|
47
|
+
import fs.subfs as subfs
|
|
48
|
+
import fs.tempfs as tempfs
|
|
49
|
+
import fs.tools as tools
|
|
50
|
+
import fs.walk as walk
|
|
51
|
+
import fs.zipfs as zipfs
|
|
52
|
+
|
|
53
|
+
_haveFS = True
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
"base",
|
|
58
|
+
"copy",
|
|
59
|
+
"errors",
|
|
60
|
+
"info",
|
|
61
|
+
"osfs",
|
|
62
|
+
"path",
|
|
63
|
+
"subfs",
|
|
64
|
+
"tempfs",
|
|
65
|
+
"tools",
|
|
66
|
+
"walk",
|
|
67
|
+
"zipfs",
|
|
68
|
+
]
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
|
|
6
|
+
from ._copy import copy_dir, copy_file
|
|
7
|
+
from ._errors import (
|
|
8
|
+
DestinationExists,
|
|
9
|
+
DirectoryExpected,
|
|
10
|
+
FileExpected,
|
|
11
|
+
FilesystemClosed,
|
|
12
|
+
NoSysPath,
|
|
13
|
+
ResourceNotFound,
|
|
14
|
+
)
|
|
15
|
+
from ._path import dirname
|
|
16
|
+
from ._walk import BoundWalker
|
|
17
|
+
|
|
18
|
+
if typing.TYPE_CHECKING:
|
|
19
|
+
from typing import IO, Any, Collection, Iterator, Self, Type
|
|
20
|
+
|
|
21
|
+
from ._info import Info
|
|
22
|
+
from ._subfs import SubFS
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FS(ABC):
|
|
26
|
+
"""Abstract base class for custom filesystems."""
|
|
27
|
+
|
|
28
|
+
_closed: bool = False
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def open(self, path: str, mode: str = "rb", **kwargs) -> IO[Any]: ...
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def exists(self, path: str) -> bool: ...
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def isdir(self, path: str) -> bool: ...
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def isfile(self, path: str) -> bool: ...
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def listdir(self, path: str) -> list[str]: ...
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def makedir(self, path: str, recreate: bool = False) -> SubFS: ...
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def makedirs(self, path: str, recreate: bool = False) -> SubFS: ...
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def getinfo(self, path: str, namespaces: Collection[str] | None = None) -> Info: ...
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def remove(self, path: str) -> None: ...
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
def removedir(self, path: str) -> None: ...
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def removetree(self, path: str) -> None: ...
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def movedir(self, src: str, dst: str, create: bool = False) -> None: ...
|
|
65
|
+
|
|
66
|
+
def getsyspath(self, path: str) -> str:
|
|
67
|
+
raise NoSysPath(f"the filesystem {self!r} has no system path")
|
|
68
|
+
|
|
69
|
+
def close(self):
|
|
70
|
+
self._closed = True
|
|
71
|
+
|
|
72
|
+
def isclosed(self) -> bool:
|
|
73
|
+
return self._closed
|
|
74
|
+
|
|
75
|
+
def __enter__(self) -> Self:
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
def __exit__(self, exc_type, exc, tb):
|
|
79
|
+
self.close()
|
|
80
|
+
return False # never swallow exceptions
|
|
81
|
+
|
|
82
|
+
def check(self):
|
|
83
|
+
if self._closed:
|
|
84
|
+
raise FilesystemClosed(f"the filesystem {self!r} is closed")
|
|
85
|
+
|
|
86
|
+
def opendir(self, path: str, *, factory: Type[SubFS] | None = None) -> SubFS:
|
|
87
|
+
"""Return a sub‑filesystem rooted at `path`."""
|
|
88
|
+
if factory is None:
|
|
89
|
+
from ._subfs import SubFS
|
|
90
|
+
|
|
91
|
+
factory = SubFS
|
|
92
|
+
return factory(self, path)
|
|
93
|
+
|
|
94
|
+
def scandir(
|
|
95
|
+
self, path: str, namespaces: Collection[str] | None = None
|
|
96
|
+
) -> Iterator[Info]:
|
|
97
|
+
return (self.getinfo(f"{path}/{p}", namespaces) for p in self.listdir(path))
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def walk(self) -> BoundWalker:
|
|
101
|
+
return BoundWalker(self)
|
|
102
|
+
|
|
103
|
+
def readbytes(self, path: str) -> bytes:
|
|
104
|
+
with self.open(path, "rb") as f:
|
|
105
|
+
return f.read()
|
|
106
|
+
|
|
107
|
+
def writebytes(self, path: str, data: bytes):
|
|
108
|
+
with self.open(path, "wb") as f:
|
|
109
|
+
f.write(data)
|
|
110
|
+
|
|
111
|
+
def create(self, path: str, wipe: bool = False):
|
|
112
|
+
if not wipe and self.exists(path):
|
|
113
|
+
return False
|
|
114
|
+
with self.open(path, "wb"):
|
|
115
|
+
pass # 'touch' empty file
|
|
116
|
+
return True
|
|
117
|
+
|
|
118
|
+
def copy(self, src_path: str, dst_path: str, overwrite=False):
|
|
119
|
+
if not self.exists(src_path):
|
|
120
|
+
raise ResourceNotFound(f"{src_path!r} does not exist")
|
|
121
|
+
elif not self.isfile(src_path):
|
|
122
|
+
raise FileExpected(f"path {src_path!r} should be a file")
|
|
123
|
+
if not overwrite and self.exists(dst_path):
|
|
124
|
+
raise DestinationExists(f"destination {dst_path!r} already exists")
|
|
125
|
+
if not self.isdir(dirname(dst_path)):
|
|
126
|
+
raise DirectoryExpected(f"path {dirname(dst_path)!r} should be a directory")
|
|
127
|
+
copy_file(self, src_path, self, dst_path)
|
|
128
|
+
|
|
129
|
+
def copydir(self, src_path: str, dst_path: str, create=False):
|
|
130
|
+
if not create and not self.exists(dst_path):
|
|
131
|
+
raise ResourceNotFound(f"{dst_path!r} does not exist")
|
|
132
|
+
if not self.isdir(src_path):
|
|
133
|
+
raise DirectoryExpected(f"path {src_path!r} should be a directory")
|
|
134
|
+
copy_dir(self, src_path, self, dst_path)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from ._errors import IllegalDestination
|
|
6
|
+
from ._path import combine, frombase, isbase
|
|
7
|
+
from ._tools import copy_file_data
|
|
8
|
+
|
|
9
|
+
if typing.TYPE_CHECKING:
|
|
10
|
+
from ._base import FS
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def copy_file(src_fs: FS, src_path: str, dst_fs: FS, dst_path: str):
|
|
14
|
+
if src_fs is dst_fs and src_path == dst_path:
|
|
15
|
+
raise IllegalDestination(f"cannot copy {src_path!r} to itself")
|
|
16
|
+
|
|
17
|
+
with src_fs.open(src_path, "rb") as src_file:
|
|
18
|
+
with dst_fs.open(dst_path, "wb") as dst_file:
|
|
19
|
+
copy_file_data(src_file, dst_file)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def copy_structure(
|
|
23
|
+
src_fs: FS,
|
|
24
|
+
dst_fs: FS,
|
|
25
|
+
src_root: str = "/",
|
|
26
|
+
dst_root: str = "/",
|
|
27
|
+
):
|
|
28
|
+
if src_fs is dst_fs and isbase(src_root, dst_root):
|
|
29
|
+
raise IllegalDestination(f"cannot copy {src_fs!r} to itself")
|
|
30
|
+
|
|
31
|
+
dst_fs.makedirs(dst_root, recreate=True)
|
|
32
|
+
for dir_path in src_fs.walk.dirs(src_root):
|
|
33
|
+
dst_fs.makedir(combine(dst_root, frombase(src_root, dir_path)), recreate=True)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def copy_dir(src_fs: FS, src_path: str, dst_fs: FS, dst_path: str):
|
|
37
|
+
copy_structure(src_fs, dst_fs, src_path, dst_path)
|
|
38
|
+
|
|
39
|
+
for file_path in src_fs.walk.files(src_path):
|
|
40
|
+
copy_path = combine(dst_path, frombase(src_path, file_path))
|
|
41
|
+
copy_file(src_fs, file_path, dst_fs, copy_path)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def copy_fs(src_fs: FS, dst_fs: FS):
|
|
45
|
+
copy_dir(src_fs, "/", dst_fs, "/")
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
class FSError(Exception):
|
|
2
|
+
pass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CreateFailed(FSError):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FilesystemClosed(FSError):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MissingInfoNamespace(FSError):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class NoSysPath(FSError):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class OperationFailed(FSError):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class IllegalDestination(OperationFailed):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ResourceError(FSError):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ResourceNotFound(ResourceError):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class DirectoryExpected(ResourceError):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class DirectoryNotEmpty(ResourceError):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class FileExpected(ResourceError):
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class DestinationExists(ResourceError):
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ResourceReadOnly(ResourceError):
|
|
54
|
+
pass
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
|
|
6
|
+
from ._errors import MissingInfoNamespace
|
|
7
|
+
|
|
8
|
+
if typing.TYPE_CHECKING:
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def epoch_to_datetime(t: int | None) -> datetime | None:
|
|
14
|
+
"""Convert epoch time to a UTC datetime."""
|
|
15
|
+
if t is None:
|
|
16
|
+
return None
|
|
17
|
+
return datetime.fromtimestamp(t, tz=timezone.utc)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Info:
|
|
21
|
+
__slots__ = ["raw", "namespaces"]
|
|
22
|
+
|
|
23
|
+
def __init__(self, raw_info: Mapping[str, Any]):
|
|
24
|
+
self.raw = raw_info
|
|
25
|
+
self.namespaces = frozenset(raw_info.keys())
|
|
26
|
+
|
|
27
|
+
def get(self, namespace: str, key: str, default: Any | None = None) -> Any | None:
|
|
28
|
+
try:
|
|
29
|
+
return self.raw[namespace].get(key, default)
|
|
30
|
+
except KeyError:
|
|
31
|
+
raise MissingInfoNamespace(f"Namespace {namespace!r} does not exist")
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def name(self) -> str:
|
|
35
|
+
return self.get("basic", "name")
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def is_dir(self) -> bool:
|
|
39
|
+
return self.get("basic", "is_dir")
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def is_file(self) -> bool:
|
|
43
|
+
return not self.is_dir
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def accessed(self) -> datetime | None:
|
|
47
|
+
return epoch_to_datetime(self.get("details", "accessed"))
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def modified(self) -> datetime | None:
|
|
51
|
+
return epoch_to_datetime(self.get("details", "modified"))
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def size(self) -> int | None:
|
|
55
|
+
return self.get("details", "size")
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def type(self) -> int | None:
|
|
59
|
+
return self.get("details", "type")
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def created(self) -> datetime | None:
|
|
63
|
+
return epoch_to_datetime(self.get("details", "created"))
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def metadata_changed(self) -> datetime | None:
|
|
67
|
+
return epoch_to_datetime(self.get("details", "metadata_changed"))
|
|
68
|
+
|
|
69
|
+
def __str__(self) -> str:
|
|
70
|
+
if self.is_dir:
|
|
71
|
+
return "<dir '{}'>".format(self.name)
|
|
72
|
+
else:
|
|
73
|
+
return "<file '{}'>".format(self.name)
|
|
74
|
+
|
|
75
|
+
__repr__ = __str__
|