31e457151bbbb0862e2551ffc3e4d9e30634a3a5
[blender-staging.git] / release / scripts / modules / bl_i18n_utils / settings.py
1 # ***** BEGIN GPL LICENSE BLOCK *****
2 #
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 #
17 # ***** END GPL LICENSE BLOCK *****
18
19 # <pep8 compliant>
20
21 # Global settings used by all scripts in this dir.
22 # XXX Before any use of the tools in this dir, please make a copy of this file
23 #     named "setting.py"
24 # XXX This is a template, most values should be OK, but some you’ll have to
25 #     edit (most probably, BLENDER_EXEC and SOURCE_DIR).
26
27
28 import json
29 import os
30 import sys
31
32 import bpy
33
34 ###############################################################################
35 # MISC
36 ###############################################################################
37
38 # The languages defined in Blender.
39 LANGUAGES_CATEGORIES = (
40     # Min completeness level, UI english label.
41     ( 0.95, "Complete"),
42     ( 0.33, "In Progress"),
43     ( -1.0, "Starting"),
44 )
45 LANGUAGES = (
46     # ID, UI english label, ISO code.
47     ( 0, "Default (Default)", "DEFAULT"),
48     ( 1, "English (English)", "en_US"),
49     ( 2, "Japanese (日本語)", "ja_JP"),
50     ( 3, "Dutch (Nederlandse taal)", "nl_NL"),
51     ( 4, "Italian (Italiano)", "it_IT"),
52     ( 5, "German (Deutsch)", "de_DE"),
53     ( 6, "Finnish (Suomi)", "fi_FI"),
54     ( 7, "Swedish (Svenska)", "sv_SE"),
55     ( 8, "French (Français)", "fr_FR"),
56     ( 9, "Spanish (Español)", "es"),
57     (10, "Catalan (Català)", "ca_AD"),
58     (11, "Czech (Český)", "cs_CZ"),
59     (12, "Portuguese (Português)", "pt_PT"),
60     (13, "Simplified Chinese (简体中文)", "zh_CN"),
61     (14, "Traditional Chinese (繁體中文)", "zh_TW"),
62     (15, "Russian (Русский)", "ru_RU"),
63     (16, "Croatian (Hrvatski)", "hr_HR"),
64     (17, "Serbian (Српски)", "sr_RS"),
65     (18, "Ukrainian (Український)", "uk_UA"),
66     (19, "Polish (Polski)", "pl_PL"),
67     (20, "Romanian (Român)", "ro_RO"),
68     # Using the utf8 flipped form of Arabic (العربية).
69     (21, "Arabic (ﺔﻴﺑﺮﻌﻟﺍ)", "ar_EG"),
70     (22, "Bulgarian (Български)", "bg_BG"),
71     (23, "Greek (Ελληνικά)", "el_GR"),
72     (24, "Korean (한국 언어)", "ko_KR"),
73     (25, "Nepali (नेपाली)", "ne_NP"),
74     # Using the utf8 flipped form of Persian (فارسی).
75     (26, "Persian (ﯽﺳﺭﺎﻓ)", "fa_IR"),
76     (27, "Indonesian (Bahasa indonesia)", "id_ID"),
77     (28, "Serbian Latin (Srpski latinica)", "sr_RS@latin"),
78     (29, "Kyrgyz (Кыргыз тили)", "ky_KG"),
79     (30, "Turkish (Türkçe)", "tr_TR"),
80     (31, "Hungarian (Magyar)", "hu_HU"),
81     (32, "Brazilian Portuguese (Português do Brasil)", "pt_BR"),
82     # Using the utf8 flipped form of Hebrew (עִבְרִית)).
83     (33, "Hebrew (תירִבְעִ)", "he_IL"),
84     (34, "Estonian (Eestlane)", "et_EE"),
85     (35, "Esperanto (Esperanto)", "eo"),
86     (36, "Spanish from Spain (Español de España)", "es_ES"),
87     (37, "Amharic (አማርኛ)", "am_ET"),
88     (38, "Uzbek (Oʻzbek)", "uz_UZ"),
89     (39, "Uzbek Cyrillic (Ўзбек)", "uz_UZ@cyrillic"),
90     (40, "Hindi (मानक हिन्दी)", "hi_IN"),
91 )
92
93 # Default context, in py!
94 DEFAULT_CONTEXT = bpy.app.translations.contexts.default
95
96 # Name of language file used by Blender to generate translations' menu.
97 LANGUAGES_FILE = "languages"
98
99 # The min level of completeness for a po file to be imported from /branches into /trunk, as a percentage.
100 IMPORT_MIN_LEVEL = 0.0
101
102 # Languages in /branches we do not want to import in /trunk currently...
103 IMPORT_LANGUAGES_SKIP = {
104     'am_ET', 'bg_BG', 'fi_FI', 'el_GR', 'et_EE', 'ne_NP', 'nl_NL', 'pl_PL', 'ro_RO', 'uz_UZ', 'uz_UZ@cyrillic',
105 }
106
107 # Languages that need RTL pre-processing.
108 IMPORT_LANGUAGES_RTL = {
109     'ar_EG', 'fa_IR', 'he_IL',
110 }
111
112 # The comment prefix used in generated messages.txt file.
113 MSG_COMMENT_PREFIX = "#~ "
114
115 # The comment prefix used in generated messages.txt file.
116 MSG_CONTEXT_PREFIX = "MSGCTXT:"
117
118 # The default comment prefix used in po's.
119 PO_COMMENT_PREFIX= "# "
120
121 # The comment prefix used to mark sources of msgids, in po's.
122 PO_COMMENT_PREFIX_SOURCE = "#: "
123
124 # The comment prefix used to mark sources of msgids, in po's.
125 PO_COMMENT_PREFIX_SOURCE_CUSTOM = "#. :src: "
126
127 # The general "generated" comment prefix, in po's.
128 PO_COMMENT_PREFIX_GENERATED = "#. "
129
130 # The comment prefix used to comment entries in po's.
131 PO_COMMENT_PREFIX_MSG= "#~ "
132
133 # The comment prefix used to mark fuzzy msgids, in po's.
134 PO_COMMENT_FUZZY = "#, fuzzy"
135
136 # The prefix used to define context, in po's.
137 PO_MSGCTXT = "msgctxt "
138
139 # The prefix used to define msgid, in po's.
140 PO_MSGID = "msgid "
141
142 # The prefix used to define msgstr, in po's.
143 PO_MSGSTR = "msgstr "
144
145 # The 'header' key of po files.
146 PO_HEADER_KEY = (DEFAULT_CONTEXT, "")
147
148 PO_HEADER_MSGSTR = (
149     "Project-Id-Version: {blender_ver} (r{blender_rev})\\n\n"
150     "Report-Msgid-Bugs-To: \\n\n"
151     "POT-Creation-Date: {time}\\n\n"
152     "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\n"
153     "Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\n"
154     "Language-Team: LANGUAGE <LL@li.org>\\n\n"
155     "Language: {uid}\\n\n"
156     "MIME-Version: 1.0\\n\n"
157     "Content-Type: text/plain; charset=UTF-8\\n\n"
158     "Content-Transfer-Encoding: 8bit\n"
159 )
160 PO_HEADER_COMMENT_COPYRIGHT = (
161     "# Blender's translation file (po format).\n"
162     "# Copyright (C) {year} The Blender Foundation.\n"
163     "# This file is distributed under the same license as the Blender package.\n"
164     "#\n"
165 )
166 PO_HEADER_COMMENT = (
167     "# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.\n"
168     "#"
169 )
170
171 TEMPLATE_ISO_ID = "__TEMPLATE__"
172
173 # Num buttons report their label with a trailing ': '...
174 NUM_BUTTON_SUFFIX = ": "
175
176 # Undocumented operator placeholder string.
177 UNDOC_OPS_STR = "(undocumented operator)"
178
179 # The gettext domain.
180 DOMAIN = "blender"
181
182 # Our own "gettext" stuff.
183 # File type (ext) to parse.
184 PYGETTEXT_ALLOWED_EXTS = {".c", ".cpp", ".cxx", ".hpp", ".hxx", ".h"}
185
186 # Max number of contexts into a BLF_I18N_MSGID_MULTI_CTXT macro...
187 PYGETTEXT_MAX_MULTI_CTXT = 16
188
189 # Where to search contexts definitions, relative to SOURCE_DIR (defined below).
190 PYGETTEXT_CONTEXTS_DEFSRC = os.path.join("source", "blender", "blenfont", "BLF_translation.h")
191
192 # Regex to extract contexts defined in BLF_translation.h
193 # XXX Not full-proof, but should be enough here!
194 PYGETTEXT_CONTEXTS = "#define\\s+(BLF_I18NCONTEXT_[A-Z_0-9]+)\\s+\"([^\"]*)\""
195
196 # Keywords' regex.
197 # XXX Most unfortunately, we can't use named backreferences inside character sets,
198 #     which makes the regexes even more twisty... :/
199 _str_base = (
200     # Match void string
201     "(?P<{_}1>[\"'])(?P={_}1)"  # Get opening quote (' or "), and closing immediately.
202     "|"
203     # Or match non-void string
204     "(?P<{_}2>[\"'])"  # Get opening quote (' or ").
205         "(?{capt}(?:"
206             # This one is for crazy things like "hi \\\\\" folks!"...
207             r"(?:(?!<\\)(?:\\\\)*\\(?=(?P={_}2)))|"
208             # The most common case.
209             ".(?!(?P={_}2))"
210         ")+.)"  # Don't forget the last char!
211     "(?P={_}2)"  # And closing quote.
212 )
213 str_clean_re = _str_base.format(_="g", capt="P<clean>")
214 _inbetween_str_re = (
215     # XXX Strings may have comments between their pieces too, not only spaces!
216     r"(?:\s*(?:"
217         # A C comment
218         r"/\*.*(?!\*/).\*/|"
219         # Or a C++ one!
220         r"//[^\n]*\n"
221     # And we are done!
222     r")?)*"
223 )
224 # Here we have to consider two different cases (empty string and other).
225 _str_whole_re = (
226     _str_base.format(_="{_}1_", capt=":") +
227     # Optional loop start, this handles "split" strings...
228     "(?:(?<=[\"'])" + _inbetween_str_re + "(?=[\"'])(?:"
229         + _str_base.format(_="{_}2_", capt=":") +
230     # End of loop.
231     "))*"
232 )
233 _ctxt_re_gen = lambda uid : r"(?P<ctxt_raw{uid}>(?:".format(uid=uid) + \
234                             _str_whole_re.format(_="_ctxt{uid}".format(uid=uid)) + \
235                             r")|(?:[A-Z_0-9]+))"
236 _ctxt_re = _ctxt_re_gen("")
237 _msg_re = r"(?P<msg_raw>" + _str_whole_re.format(_="_msg") + r")"
238 PYGETTEXT_KEYWORDS = (() +
239     tuple((r"{}\(\s*" + _msg_re + r"\s*\)").format(it)
240           for it in ("IFACE_", "TIP_", "DATA_", "N_")) +
241
242     tuple((r"{}\(\s*" + _ctxt_re + r"\s*,\s*" + _msg_re + r"\s*\)").format(it)
243           for it in ("CTX_IFACE_", "CTX_TIP_", "CTX_DATA_", "CTX_N_")) + 
244
245     tuple(("{}\\((?:[^\"',]+,){{1,2}}\\s*" + _msg_re + r"\s*(?:\)|,)").format(it)
246           for it in ("BKE_report", "BKE_reportf", "BKE_reports_prepend", "BKE_reports_prependf",
247                      "CTX_wm_operator_poll_msg_set")) +
248
249     tuple(("{}\\((?:[^\"',]+,){{3}}\\s*" + _msg_re + r"\s*\)").format(it)
250           for it in ("BMO_error_raise",)) +
251
252     tuple(("{}\\((?:[^\"',]+,)\\s*" + _msg_re + r"\s*(?:\)|,)").format(it)
253           for it in ("modifier_setError",)) +
254
255     tuple((r"{}\(\s*" + _msg_re + r"\s*,\s*(?:" + \
256            r"\s*,\s*)?(?:".join(_ctxt_re_gen(i) for i in range(PYGETTEXT_MAX_MULTI_CTXT)) + r")?\s*\)").format(it)
257           for it in ("BLF_I18N_MSGID_MULTI_CTXT",))
258 )
259
260 # Should po parser warn when finding a first letter not capitalized?
261 WARN_MSGID_NOT_CAPITALIZED = True
262
263 # Strings that should not raise above warning!
264 WARN_MSGID_NOT_CAPITALIZED_ALLOWED = {
265     "",                              # Simplifies things... :p
266     "ac3",
267     "along X",
268     "along Y",
269     "along Z",
270     "along %s X",
271     "along %s Y",
272     "along %s Z",
273     "along local Z",
274     "ascii",
275     "author",                        # Addons' field. :/
276     "bItasc",
277     "description",                   # Addons' field. :/
278     "dx",
279     "fBM",
280     "flac",
281     "fps: %.2f",
282     "fps: %i",
283     "fStop",
284     "gimbal",
285     "global",
286     "iScale",
287     "iso-8859-15",
288     "iTaSC",
289     "iTaSC parameters",
290     "kb",
291     "local",
292     "location",                      # Addons' field. :/
293     "locking %s X",
294     "locking %s Y",
295     "locking %s Z",
296     "mkv",
297     "mm",
298     "mp2",
299     "mp3",
300     "normal",
301     "ogg",
302     "p0",
303     "px",
304     "re",
305     "res",
306     "rv",
307     "sin(x) / x",
308     "sqrt(x*x+y*y+z*z)",
309     "sRGB",
310     "utf-8",
311     "var",
312     "vBVH",
313     "view",
314     "wav",
315     "y",
316     # Sub-strings.
317     "available with",
318     "can't save image while rendering",
319     "expected a timeline/animation area to be active",
320     "expected a view3d region",
321     "expected a view3d region & editcurve",
322     "expected a view3d region & editmesh",
323     "image file not found",
324     "image path can't be written to",
325     "in memory to enable editing!",
326     "unable to load movie clip",
327     "unable to load text",
328     "unable to open the file",
329     "unknown error reading file",
330     "unknown error stating file",
331     "unknown error writing file",
332     "unsupported font format",
333     "unsupported format",
334     "unsupported image format",
335     "unsupported movie clip format",
336     "verts only",
337     "virtual parents",
338 }
339 WARN_MSGID_NOT_CAPITALIZED_ALLOWED |= set(lng[2] for lng in LANGUAGES)
340
341 WARN_MSGID_END_POINT_ALLOWED = {
342     "Numpad .",
343     "Circle|Alt .",
344     "Temp. Diff.",
345     "Float Neg. Exp.",
346     "    RNA Path: bpy.types.",
347 }
348
349 PARSER_CACHE_HASH = 'sha1'
350
351 PARSER_TEMPLATE_ID = "__POT__"
352 PARSER_PY_ID = "__PY__"
353
354 PARSER_PY_MARKER_BEGIN = "\n# ##### BEGIN AUTOGENERATED I18N SECTION #####\n"
355 PARSER_PY_MARKER_END = "\n# ##### END AUTOGENERATED I18N SECTION #####\n"
356
357 PARSER_MAX_FILE_SIZE = 2**24  # in bytes, i.e. 16 Mb.
358
359 ###############################################################################
360 # PATHS
361 ###############################################################################
362
363 # The Python3 executable.You’ll likely have to edit it in your user_settings.py
364 # if you’re under Windows.
365 PYTHON3_EXEC = "python3"
366
367 # The Blender executable!
368 # This is just an example, you’ll have to edit it in your user_settings.py!
369 BLENDER_EXEC = os.path.abspath(os.path.join("foo", "bar", "blender"))
370 # check for blender.bin
371 if not os.path.exists(BLENDER_EXEC):
372     if os.path.exists(BLENDER_EXEC + ".bin"):
373         BLENDER_EXEC = BLENDER_EXEC + ".bin"
374
375 # The gettext msgfmt "compiler". You’ll likely have to edit it in your user_settings.py if you’re under Windows.
376 GETTEXT_MSGFMT_EXECUTABLE = "msgfmt"
377
378 # The FriBidi C compiled library (.so under Linux, .dll under windows...).
379 # You’ll likely have to edit it in your user_settings.py if you’re under Windows., e.g. using the included one:
380 #     FRIBIDI_LIB = os.path.join(TOOLS_DIR, "libfribidi.dll")
381 FRIBIDI_LIB = "libfribidi.so.0"
382
383 # The name of the (currently empty) file that must be present in a po's directory to enable rtl-preprocess.
384 RTL_PREPROCESS_FILE = "is_rtl"
385
386 # The Blender source root path.
387 # This is just an example, you’ll have to override it in your user_settings.py!
388 SOURCE_DIR = os.path.abspath(os.path.join("blender"))
389
390 # The bf-translation repository (you'll have to override this in your user_settings.py).
391 I18N_DIR = os.path.abspath(os.path.join("i18n"))
392
393 # The /branches path (relative to I18N_DIR).
394 REL_BRANCHES_DIR = os.path.join("branches")
395
396 # The /trunk path (relative to I18N_DIR).
397 REL_TRUNK_DIR = os.path.join("trunk")
398
399 # The /trunk/po path (relative to I18N_DIR).
400 REL_TRUNK_PO_DIR = os.path.join(REL_TRUNK_DIR, "po")
401
402 # The /trunk/mo path (relative to I18N_DIR).
403 REL_TRUNK_MO_DIR = os.path.join(REL_TRUNK_DIR, "locale")
404
405 # The Blender source path to check for i18n macros (relative to SOURCE_DIR).
406 REL_POTFILES_SOURCE_DIR = os.path.join("source")
407
408 # The template messages file (relative to I18N_DIR).
409 REL_FILE_NAME_POT = os.path.join(REL_BRANCHES_DIR, DOMAIN + ".pot")
410
411 # Mo root datapath.
412 REL_MO_PATH_ROOT = os.path.join(REL_TRUNK_DIR, "locale")
413
414 # Mo path generator for a given language.
415 REL_MO_PATH_TEMPLATE = os.path.join(REL_MO_PATH_ROOT, "{}", "LC_MESSAGES")
416
417 # Mo path generator for a given language (relative to any "locale" dir).
418 MO_PATH_ROOT_RELATIVE = os.path.join("locale")
419 MO_PATH_TEMPLATE_RELATIVE = os.path.join(MO_PATH_ROOT_RELATIVE, "{}", "LC_MESSAGES")
420
421 # Mo file name.
422 MO_FILE_NAME = DOMAIN + ".mo"
423
424 # Where to search for py files that may contain ui strings (relative to one of the 'resource_path' of Blender).
425 CUSTOM_PY_UI_FILES = [
426     os.path.join("scripts", "startup", "bl_ui"),
427     os.path.join("scripts", "modules", "rna_prop_ui.py"),
428 ]
429
430 # An optional text file listing files to force include/exclude from py_xgettext process.
431 SRC_POTFILES = ""
432
433 # A cache storing validated msgids, to avoid re-spellchecking them.
434 SPELL_CACHE = os.path.join("/tmp", ".spell_cache")
435
436 # Threshold defining whether a new msgid is similar enough with an old one to reuse its translation...
437 SIMILAR_MSGID_THRESHOLD = 0.75
438
439 # Additional import paths to add to sys.path (';' separated)...
440 INTERN_PY_SYS_PATHS = ""
441
442 # Custom override settings must be one dir above i18n tools itself!
443 sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
444 try:
445     from bl_i18n_settings_override import *
446 except ImportError:  # If no i18n_override_settings available, it’s no error!
447     pass
448
449 # Override with custom user settings, if available.
450 try:
451     from settings_user import *
452 except ImportError:  # If no user_settings available, it’s no error!
453     pass
454
455
456 for p in set(INTERN_PY_SYS_PATHS.split(";")):
457     if p:
458         sys.path.append(p)
459
460
461 # The settings class itself!
462 def _do_get(ref, path):
463     return os.path.normpath(os.path.join(ref, path))
464
465 def _do_set(ref, path):
466     path = os.path.normpath(path)
467     # If given path is absolute, make it relative to current ref one (else we consider it is already the case!)
468     if os.path.isabs(path):
469         return os.path.relpath(path, ref)
470     else:
471         return path
472
473 def _gen_get_set_path(ref, name):
474     def _get(self):
475         return _do_get(getattr(self, ref), getattr(self, name))
476     def _set(self, value):
477         setattr(self, name, _do_set(getattr(self, ref), value))
478     return _get, _set
479
480 def _gen_get_set_paths(ref, name):
481     def _get(self):
482         return [_do_get(getattr(self, ref), p) for p in getattr(self, name)]
483     def _set(self, value):
484         setattr(self, name, [_do_set(getattr(self, ref), p) for p in value])
485     return _get, _set
486
487 class I18nSettings:
488     """
489     Class allowing persistence of our settings!
490     Saved in JSon format, so settings should be JSon'able objects!
491     """
492     _settings = None
493
494     def __new__(cls, *args, **kwargs):
495         # Addon preferences are singleton by definition, so is this class!
496         if not I18nSettings._settings:
497             cls._settings = super(I18nSettings, cls).__new__(cls)
498             cls._settings.__dict__ = {uid: data for uid, data in globals().items() if not uid.startswith("_")}
499         return I18nSettings._settings
500
501     def from_json(self, string):
502         data = dict(json.loads(string))
503         # Special case... :/
504         if "INTERN_PY_SYS_PATHS" in data:
505             self.PY_SYS_PATHS = data["INTERN_PY_SYS_PATHS"]
506         self.__dict__.update(data)
507
508     def to_json(self):
509         # Only save the diff from default i18n_settings!
510         glob = globals()
511         export_dict = {uid: val for uid, val in self.__dict__.items() if glob.get(uid) != val}
512         return json.dumps(export_dict)
513
514     def load(self, fname, reset=False):
515         if reset:
516             self.__dict__ = {uid: data for uid, data in globals().items() if not uid.startswith("_")}
517         if isinstance(fname, str):
518             if not os.path.isfile(fname):
519                 return
520             with open(fname) as f:
521                 self.from_json(f.read())
522         # Else assume fname is already a file(like) object!
523         else:
524             self.from_json(fname.read())
525
526     def save(self, fname):
527         if isinstance(fname, str):
528             with open(fname, 'w') as f:
529                 f.write(self.to_json())
530         # Else assume fname is already a file(like) object!
531         else:
532             fname.write(self.to_json())
533
534     BRANCHES_DIR = property(*(_gen_get_set_path("I18N_DIR", "REL_BRANCHES_DIR")))
535     TRUNK_DIR = property(*(_gen_get_set_path("I18N_DIR", "REL_TRUNK_DIR")))
536     TRUNK_PO_DIR = property(*(_gen_get_set_path("I18N_DIR", "REL_TRUNK_PO_DIR")))
537     TRUNK_MO_DIR = property(*(_gen_get_set_path("I18N_DIR", "REL_TRUNK_MO_DIR")))
538     POTFILES_SOURCE_DIR = property(*(_gen_get_set_path("SOURCE_DIR", "REL_POTFILES_SOURCE_DIR")))
539     FILE_NAME_POT = property(*(_gen_get_set_path("I18N_DIR", "REL_FILE_NAME_POT")))
540     MO_PATH_ROOT = property(*(_gen_get_set_path("I18N_DIR", "REL_MO_PATH_ROOT")))
541     MO_PATH_TEMPLATE = property(*(_gen_get_set_path("I18N_DIR", "REL_MO_PATH_TEMPLATE")))
542
543     def _get_py_sys_paths(self):
544         return self.INTERN_PY_SYS_PATHS
545     def _set_py_sys_paths(self, val):
546         old_paths = set(self.INTERN_PY_SYS_PATHS.split(";")) - {""}
547         new_paths = set(val.split(";")) - {""}
548         for p in old_paths - new_paths:
549             if p in sys.path:
550                 sys.path.remove(p)
551         for p in new_paths - old_paths:
552             sys.path.append(p)
553         self.INTERN_PY_SYS_PATHS = val
554     PY_SYS_PATHS = property(_get_py_sys_paths, _set_py_sys_paths)