comparison python-daemon-2.0.5/version.py @ 33:7ceb967147c3

start xena with no gui add library files
author jingchunzhu <jingchunzhu@gmail.com>
date Wed, 22 Jul 2015 13:24:44 -0700
parents
children
comparison
equal deleted inserted replaced
32:63b1ba1e3424 33:7ceb967147c3
1 # -*- coding: utf-8 -*-
2
3 # version.py
4 # Part of ‘python-daemon’, an implementation of PEP 3143.
5 #
6 # Copyright © 2008–2015 Ben Finney <ben+python@benfinney.id.au>
7 #
8 # This is free software: you may copy, modify, and/or distribute this work
9 # under the terms of the GNU General Public License as published by the
10 # Free Software Foundation; version 3 of that license or any later version.
11 # No warranty expressed or implied. See the file ‘LICENSE.GPL-3’ for details.
12
13 """ Version information unified for human- and machine-readable formats.
14
15 The project ‘ChangeLog’ file is a reStructuredText document, with
16 each section describing a version of the project. The document is
17 intended to be readable as-is by end users.
18
19 This module handles transformation from the ‘ChangeLog’ to a
20 mapping of version information, serialised as JSON. It also
21 provides functionality for Distutils to use this information.
22
23 Requires:
24
25 * Docutils <http://docutils.sourceforge.net/>
26 * JSON <https://docs.python.org/3/reference/json.html>
27
28 """
29
30 from __future__ import (absolute_import, unicode_literals)
31
32 import sys
33 import os
34 import io
35 import errno
36 import json
37 import datetime
38 import textwrap
39 import re
40 import functools
41 import collections
42 import distutils
43 import distutils.errors
44 import distutils.cmd
45 try:
46 # Python 2 has both ‘str’ (bytes) and ‘unicode’ (text).
47 basestring = basestring
48 unicode = unicode
49 except NameError:
50 # Python 3 names the Unicode data type ‘str’.
51 basestring = str
52 unicode = str
53
54 import setuptools
55 import setuptools.command.egg_info
56
57
58 def ensure_class_bases_begin_with(namespace, class_name, base_class):
59 """ Ensure the named class's bases start with the base class.
60
61 :param namespace: The namespace containing the class name.
62 :param class_name: The name of the class to alter.
63 :param base_class: The type to be the first base class for the
64 newly created type.
65 :return: ``None``.
66
67 This function is a hack to circumvent a circular dependency:
68 using classes from a module which is not installed at the time
69 this module is imported.
70
71 Call this function after ensuring `base_class` is available,
72 before using the class named by `class_name`.
73
74 """
75 existing_class = namespace[class_name]
76 assert isinstance(existing_class, type)
77
78 bases = list(existing_class.__bases__)
79 if base_class is bases[0]:
80 # Already bound to a type with the right bases.
81 return
82 bases.insert(0, base_class)
83
84 new_class_namespace = existing_class.__dict__.copy()
85 # Type creation will assign the correct ‘__dict__’ attribute.
86 del new_class_namespace['__dict__']
87
88 metaclass = existing_class.__metaclass__
89 new_class = metaclass(class_name, tuple(bases), new_class_namespace)
90
91 namespace[class_name] = new_class
92
93
94 class VersionInfoWriter(object):
95 """ Docutils writer to produce a version info JSON data stream. """
96
97 # This class needs its base class to be a class from `docutils`.
98 # But that would create a circular dependency: Setuptools cannot
99 # ensure `docutils` is available before importing this module.
100 #
101 # Use `ensure_class_bases_begin_with` after importing `docutils`, to
102 # re-bind the `VersionInfoWriter` name to a new type that inherits
103 # from `docutils.writers.Writer`.
104
105 __metaclass__ = type
106
107 supported = ['version_info']
108 """ Formats this writer supports. """
109
110 def __init__(self):
111 super(VersionInfoWriter, self).__init__()
112 self.translator_class = VersionInfoTranslator
113
114 def translate(self):
115 visitor = self.translator_class(self.document)
116 self.document.walkabout(visitor)
117 self.output = visitor.astext()
118
119
120 rfc822_person_regex = re.compile(
121 "^(?P<name>[^<]+) <(?P<email>[^>]+)>$")
122
123 class ChangeLogEntry:
124 """ An individual entry from the ‘ChangeLog’ document. """
125
126 __metaclass__ = type
127
128 field_names = [
129 'release_date',
130 'version',
131 'maintainer',
132 'body',
133 ]
134
135 date_format = "%Y-%m-%d"
136 default_version = "UNKNOWN"
137 default_release_date = "UNKNOWN"
138
139 def __init__(
140 self,
141 release_date=default_release_date, version=default_version,
142 maintainer=None, body=None):
143 self.validate_release_date(release_date)
144 self.release_date = release_date
145
146 self.version = version
147
148 self.validate_maintainer(maintainer)
149 self.maintainer = maintainer
150 self.body = body
151
152 @classmethod
153 def validate_release_date(cls, value):
154 """ Validate the `release_date` value.
155
156 :param value: The prospective `release_date` value.
157 :return: ``None`` if the value is valid.
158 :raises ValueError: If the value is invalid.
159
160 """
161 if value in ["UNKNOWN", "FUTURE"]:
162 # A valid non-date value.
163 return None
164
165 # Raises `ValueError` if parse fails.
166 datetime.datetime.strptime(value, ChangeLogEntry.date_format)
167
168 @classmethod
169 def validate_maintainer(cls, value):
170 """ Validate the `maintainer` value.
171
172 :param value: The prospective `maintainer` value.
173 :return: ``None`` if the value is valid.
174 :raises ValueError: If the value is invalid.
175
176 """
177 valid = False
178
179 if value is None:
180 valid = True
181 elif rfc822_person_regex.search(value):
182 valid = True
183
184 if not valid:
185 raise ValueError("Not a valid person specification {value!r}")
186 else:
187 return None
188
189 @classmethod
190 def make_ordered_dict(cls, fields):
191 """ Make an ordered dict of the fields. """
192 result = collections.OrderedDict(
193 (name, fields[name])
194 for name in cls.field_names)
195 return result
196
197 def as_version_info_entry(self):
198 """ Format the changelog entry as a version info entry. """
199 fields = vars(self)
200 entry = self.make_ordered_dict(fields)
201
202 return entry
203
204
205 class InvalidFormatError(ValueError):
206 """ Raised when the document is not a valid ‘ChangeLog’ document. """
207
208
209 class VersionInfoTranslator(object):
210 """ Translator from document nodes to a version info stream. """
211
212 # This class needs its base class to be a class from `docutils`.
213 # But that would create a circular dependency: Setuptools cannot
214 # ensure `docutils` is available before importing this module.
215 #
216 # Use `ensure_class_bases_begin_with` after importing `docutils`,
217 # to re-bind the `VersionInfoTranslator` name to a new type that
218 # inherits from `docutils.nodes.SparseNodeVisitor`.
219
220 __metaclass__ = type
221
222 wrap_width = 78
223 bullet_text = "* "
224
225 attr_convert_funcs_by_attr_name = {
226 'released': ('release_date', unicode),
227 'version': ('version', unicode),
228 'maintainer': ('maintainer', unicode),
229 }
230
231 def __init__(self, document):
232 super(VersionInfoTranslator, self).__init__(document)
233 self.settings = document.settings
234 self.current_section_level = 0
235 self.current_field_name = None
236 self.content = []
237 self.indent_width = 0
238 self.initial_indent = ""
239 self.subsequent_indent = ""
240 self.current_entry = None
241
242 # Docutils is not available when this class is defined.
243 # Get the `docutils` module dynamically.
244 self._docutils = sys.modules['docutils']
245
246 def astext(self):
247 """ Return the translated document as text. """
248 text = json.dumps(self.content, indent=4)
249 return text
250
251 def append_to_current_entry(self, text):
252 if self.current_entry is not None:
253 if self.current_entry.body is not None:
254 self.current_entry.body += text
255
256 def visit_Text(self, node):
257 raw_text = node.astext()
258 text = textwrap.fill(
259 raw_text,
260 width=self.wrap_width,
261 initial_indent=self.initial_indent,
262 subsequent_indent=self.subsequent_indent)
263 self.append_to_current_entry(text)
264
265 def depart_Text(self, node):
266 pass
267
268 def visit_comment(self, node):
269 raise self._docutils.nodes.SkipNode
270
271 def visit_field_body(self, node):
272 field_list_node = node.parent.parent
273 if not isinstance(field_list_node, self._docutils.nodes.field_list):
274 raise InvalidFormatError(
275 "Unexpected field within {node!r}".format(
276 node=field_list_node))
277 (attr_name, convert_func) = self.attr_convert_funcs_by_attr_name[
278 self.current_field_name]
279 attr_value = convert_func(node.astext())
280 setattr(self.current_entry, attr_name, attr_value)
281
282 def depart_field_body(self, node):
283 pass
284
285 def visit_field_list(self, node):
286 pass
287
288 def depart_field_list(self, node):
289 self.current_field_name = None
290 self.current_entry.body = ""
291
292 def visit_field_name(self, node):
293 field_name = node.astext()
294 if self.current_section_level == 1:
295 # At a top-level section.
296 if field_name.lower() not in ["released", "maintainer"]:
297 raise InvalidFormatError(
298 "Unexpected field name {name!r}".format(name=field_name))
299 self.current_field_name = field_name.lower()
300
301 def depart_field_name(self, node):
302 pass
303
304 def visit_bullet_list(self, node):
305 self.current_context = []
306
307 def depart_bullet_list(self, node):
308 self.current_entry.changes = self.current_context
309 self.current_context = None
310
311 def adjust_indent_width(self, delta):
312 self.indent_width += delta
313 self.subsequent_indent = " " * self.indent_width
314 self.initial_indent = self.subsequent_indent
315
316 def visit_list_item(self, node):
317 indent_delta = +len(self.bullet_text)
318 self.adjust_indent_width(indent_delta)
319 self.initial_indent = self.subsequent_indent[:-indent_delta]
320 self.append_to_current_entry(self.initial_indent + self.bullet_text)
321
322 def depart_list_item(self, node):
323 indent_delta = +len(self.bullet_text)
324 self.adjust_indent_width(-indent_delta)
325 self.append_to_current_entry("\n")
326
327 def visit_section(self, node):
328 self.current_section_level += 1
329 if self.current_section_level == 1:
330 # At a top-level section.
331 self.current_entry = ChangeLogEntry()
332 else:
333 raise InvalidFormatError(
334 "Subsections not implemented for this writer")
335
336 def depart_section(self, node):
337 self.current_section_level -= 1
338 self.content.append(
339 self.current_entry.as_version_info_entry())
340 self.current_entry = None
341
342 _expected_title_word_length = len("Version FOO".split(" "))
343
344 def depart_title(self, node):
345 title_text = node.astext()
346 # At a top-level section.
347 words = title_text.split(" ")
348 version = None
349 if len(words) != self._expected_title_word_length:
350 raise InvalidFormatError(
351 "Unexpected title text {text!r}".format(text=title_text))
352 if words[0].lower() not in ["version"]:
353 raise InvalidFormatError(
354 "Unexpected title text {text!r}".format(text=title_text))
355 version = words[-1]
356 self.current_entry.version = version
357
358
359 def changelog_to_version_info_collection(infile):
360 """ Render the ‘ChangeLog’ document to a version info collection.
361
362 :param infile: A file-like object containing the changelog.
363 :return: The serialised JSON data of the version info collection.
364
365 """
366
367 # Docutils is not available when Setuptools needs this module, so
368 # delay the imports to this function instead.
369 import docutils.core
370 import docutils.nodes
371 import docutils.writers
372
373 ensure_class_bases_begin_with(
374 globals(), str('VersionInfoWriter'), docutils.writers.Writer)
375 ensure_class_bases_begin_with(
376 globals(), str('VersionInfoTranslator'),
377 docutils.nodes.SparseNodeVisitor)
378
379 writer = VersionInfoWriter()
380 settings_overrides = {
381 'doctitle_xform': False,
382 }
383 version_info_json = docutils.core.publish_string(
384 infile.read(), writer=writer,
385 settings_overrides=settings_overrides)
386
387 return version_info_json
388
389
390 try:
391 lru_cache = functools.lru_cache
392 except AttributeError:
393 # Python < 3.2 does not have the `functools.lru_cache` function.
394 # Not essential, so replace it with a no-op.
395 lru_cache = lambda maxsize=None, typed=False: lambda func: func
396
397
398 @lru_cache(maxsize=128)
399 def generate_version_info_from_changelog(infile_path):
400 """ Get the version info for the latest version in the changelog.
401
402 :param infile_path: Filesystem path to the input changelog file.
403 :return: The generated version info mapping; or ``None`` if the
404 file cannot be read.
405
406 The document is explicitly opened as UTF-8 encoded text.
407
408 """
409 version_info = collections.OrderedDict()
410
411 versions_all_json = None
412 try:
413 with io.open(infile_path, 'rt', encoding="utf-8") as infile:
414 versions_all_json = changelog_to_version_info_collection(infile)
415 except EnvironmentError:
416 # If we can't read the input file, leave the collection empty.
417 pass
418
419 if versions_all_json is not None:
420 versions_all = json.loads(versions_all_json.decode('utf-8'))
421 version_info = get_latest_version(versions_all)
422
423 return version_info
424
425
426 def get_latest_version(versions):
427 """ Get the latest version from a collection of changelog entries.
428
429 :param versions: A collection of mappings for changelog entries.
430 :return: An ordered mapping of fields for the latest version,
431 if `versions` is non-empty; otherwise, an empty mapping.
432
433 """
434 version_info = collections.OrderedDict()
435
436 versions_by_release_date = {
437 item['release_date']: item
438 for item in versions}
439 if versions_by_release_date:
440 latest_release_date = max(versions_by_release_date.keys())
441 version_info = ChangeLogEntry.make_ordered_dict(
442 versions_by_release_date[latest_release_date])
443
444 return version_info
445
446
447 def serialise_version_info_from_mapping(version_info):
448 """ Generate the version info serialised data.
449
450 :param version_info: Mapping of version info items.
451 :return: The version info serialised to JSON.
452
453 """
454 content = json.dumps(version_info, indent=4)
455
456 return content
457
458
459 changelog_filename = "ChangeLog"
460
461 def get_changelog_path(distribution, filename=changelog_filename):
462 """ Get the changelog file path for the distribution.
463
464 :param distribution: The distutils.dist.Distribution instance.
465 :param filename: The base filename of the changelog document.
466 :return: Filesystem path of the changelog document, or ``None``
467 if not discoverable.
468
469 """
470 setup_dirname = os.path.dirname(distribution.script_name)
471 filepath = os.path.join(setup_dirname, filename)
472
473 return filepath
474
475
476 def has_changelog(command):
477 """ Return ``True`` iff the distribution's changelog file exists. """
478 result = False
479
480 changelog_path = get_changelog_path(command.distribution)
481 if changelog_path is not None:
482 if os.path.exists(changelog_path):
483 result = True
484
485 return result
486
487
488 class EggInfoCommand(setuptools.command.egg_info.egg_info, object):
489 """ Custom ‘egg_info’ command for this distribution. """
490
491 sub_commands = ([
492 ('write_version_info', has_changelog),
493 ] + setuptools.command.egg_info.egg_info.sub_commands)
494
495 def run(self):
496 """ Execute this command. """
497 super(EggInfoCommand, self).run()
498
499 for command_name in self.get_sub_commands():
500 self.run_command(command_name)
501
502
503 version_info_filename = "version_info.json"
504
505 class WriteVersionInfoCommand(EggInfoCommand, object):
506 """ Setuptools command to serialise version info metadata. """
507
508 user_options = ([
509 ("changelog-path=", None,
510 "Filesystem path to the changelog document."),
511 ("outfile-path=", None,
512 "Filesystem path to the version info file."),
513 ] + EggInfoCommand.user_options)
514
515 def initialize_options(self):
516 """ Initialise command options to defaults. """
517 super(WriteVersionInfoCommand, self).initialize_options()
518 self.changelog_path = None
519 self.outfile_path = None
520
521 def finalize_options(self):
522 """ Finalise command options before execution. """
523 self.set_undefined_options(
524 'build',
525 ('force', 'force'))
526
527 super(WriteVersionInfoCommand, self).finalize_options()
528
529 if self.changelog_path is None:
530 self.changelog_path = get_changelog_path(self.distribution)
531
532 if self.outfile_path is None:
533 egg_dir = self.egg_info
534 self.outfile_path = os.path.join(egg_dir, version_info_filename)
535
536 def run(self):
537 """ Execute this command. """
538 version_info = generate_version_info_from_changelog(self.changelog_path)
539 content = serialise_version_info_from_mapping(version_info)
540 self.write_file("version info", self.outfile_path, content)
541
542
543 # Local variables:
544 # coding: utf-8
545 # mode: python
546 # End:
547 # vim: fileencoding=utf-8 filetype=python :