From: "André Erdmann" <dywi@mailerd.de>
To: gentoo-commits@lists.gentoo.org
Subject: [gentoo-commits] proj/R_overlay:master commit in: roverlay/
Date: Fri, 1 Jun 2012 15:46:35 +0000 (UTC) [thread overview]
Message-ID: <1338562569.ac0b226fd7d16dad7a4b8d30b4293fe401406c1c.dywi@gentoo> (raw)
commit: ac0b226fd7d16dad7a4b8d30b4293fe401406c1c
Author: André Erdmann <dywi <AT> mailerd <DOT> de>
AuthorDate: Fri Jun 1 14:56:09 2012 +0000
Commit: André Erdmann <dywi <AT> mailerd <DOT> de>
CommitDate: Fri Jun 1 14:56:09 2012 +0000
URL: http://git.overlays.gentoo.org/gitweb/?p=proj/R_overlay.git;a=commit;h=ac0b226f
2012-01-06:
* description field definition is now configurable
modified: roverlay/__init__.py
modified: roverlay/config.py
new file: roverlay/const.py
modified: roverlay/descriptionfields.py
renamed: roverlay/fileio.py -> roverlay/descriptionreader.py
modified: roverlay/ebuild.py
modified: roverlay/ebuildjob.py
deleted: roverlay/tmpconst.py
---
roverlay/__init__.py | 5 +
roverlay/config.py | 164 ++++++++++++-
roverlay/const.py | 51 ++++
roverlay/descriptionfields.py | 343 +++++++++++++++++++++++---
roverlay/{fileio.py => descriptionreader.py} | 248 ++++++-------------
roverlay/ebuild.py | 5 +-
roverlay/ebuildjob.py | 15 +-
roverlay/tmpconst.py | 108 --------
8 files changed, 615 insertions(+), 324 deletions(-)
diff --git a/roverlay/__init__.py b/roverlay/__init__.py
index b1557ef..f50cec9 100644
--- a/roverlay/__init__.py
+++ b/roverlay/__init__.py
@@ -4,6 +4,11 @@
import logging
+
+from roverlay import config
+
+config.access().load_field_definition ( 'description_fields.conf' )
+
logging.basicConfig (
level=logging.DEBUG,
filename='roverlay.log',
diff --git a/roverlay/config.py b/roverlay/config.py
index 248fc22..b0bf7a3 100644
--- a/roverlay/config.py
+++ b/roverlay/config.py
@@ -3,20 +3,53 @@
# Distributed under the terms of the GNU General Public License v2
import sys
-
-from roverlay import descriptionfields
+import shlex
try:
import configparser
-except ImportError:
+except ImportError as running_python2:
+ # configparser is named ConfigParser in python2
import ConfigParser as configparser
+
+
+
+from roverlay import descriptionfields
+from roverlay import const
+
+
+
+
def access():
+ """Returns the ConfigTree."""
return ConfigTree() if ConfigTree.instance is None else ConfigTree.instance
+# --- end of access (...) ---
+
+
+def get ( key, fallback_value=None ):
+ """Searches for key in the ConfigTree and returns its value if possible,
+ else fallback_value.
+ 'key' is a config path [<section>[.<subsection>*]]<option name>.
+
+ arguments:
+ * key --
+ * fallback_value --
+ """
+ if fallback_value:
+ return access().get ( key, fallback_value )
+ else:
+ return access().get ( key )
+# --- end of get (...) ---
+
class InitialLogger:
def __init__ ( self ):
+ """Initializes an InitialLogger.
+ It implements the debug/info/warning/error/critical/exception methods
+ known from the logging module and its output goes directly to sys.stderr.
+ This can be used until the real logging has been configured.
+ """
self.debug = lambda x : sys.stderr.write ( "DBG " + str ( x ) + "\n" )
self.info = lambda x : sys.stderr.write ( "INFO " + str ( x ) + "\n" )
self.warning = lambda x : sys.stderr.write ( "WARN " + str ( x ) + "\n" )
@@ -24,11 +57,23 @@ class InitialLogger:
self.critical = lambda x : sys.stderr.write ( "CRIT " + str ( x ) + "\n" )
self.exception = lambda x : sys.stderr.write ( "EXC! " + str ( x ) + "\n" )
+ # --- end of __init__ (...) ---
+
class ConfigTree:
# static access to the first created ConfigTree
instance = None
- def __init__ ( self ):
+ def __init__ ( self, import_const=True ):
+ """Initializes an ConfigTree, which is a container for options/config values.
+ values can be stored directly (such as the field_definitions) or in a
+ tree-like { section -> subsection[s] -> option = value } structure.
+ Config keys cannot contain dots because they're used as config path
+ separator.
+
+ arguments:
+ * import_const -- whether to deepcopy constants into the config tree or
+ not. Copying allows faster lookups.
+ """
if ConfigTree.instance is None:
ConfigTree.instance = self
@@ -36,8 +81,52 @@ class ConfigTree:
self.parser = dict()
+ self._config = const.clone() if import_const else None
+ self._const_imported = import_const
+ self._field_definitions = None
+
+ # --- end of __init__ (...) ---
+
+
+ def get ( self, key, fallback_value=None ):
+ """Searches for key in the ConfigTree and returns its value.
+ Searches in const if ConfigTree does not contain the requested key and
+ returns the fallback_value if key not found.
+
+ arguments:
+ * key --
+ * fallback_value --
+ """
+ if self._config:
+ config_path = key.split ( '.' )
+ config_path.reverse ()
+
+ config_position = self._config
+ while len ( config_path ) and config_position:
+ next_key = config_path.pop ()
+ if next_key in config_position:
+ config_position = config_position [next_key]
+ else:
+ config_position = None
+
+ if config_position:
+ return config_position
+
+ if self._const_imported:
+ return fallback_value
+ else:
+ return const.lookup ( key, fallback_value )
+
+ # --- end of get (...) ---
def load_field_definition ( self, def_file, lenient=False ):
+ """Loads a field definition file. Please see the example file for format
+ details.
+
+ arguments:
+ * def_file -- file (str) to read, this can be a list of str if lenient is True
+ * lenient -- if True: do not fail if a file cannot be read; defaults to False
+ """
if not 'field_def' in self.parser:
self.parser ['field_def'] = configparser.SafeConfigParser ( allow_no_value=True )
@@ -57,4 +146,71 @@ class ConfigTree:
self.logger.exception ( mshe )
raise
+ # --- end of load_field_definition (...) ---
+
+
+ def get_field_definition ( self, force_update=False ):
+ """Gets the field definition stored in this ConfigTree.
+
+ arguments:
+ * force_update -- enforces recreation of the field definition data.
+ """
+ if force_update or not self._field_definitions:
+ self._field_definitions = self._make_field_definition ()
+
+ return self._field_definitions
+
+ # --- end of get_field_definition (...) ---
+
+
+ def _make_field_definition ( self ):
+ """Creates and returns field definition data. Please see the example
+ field definition config file for details.
+ """
+
+ def get_list ( value_str ):
+ if value_str is None:
+ return []
+ else:
+ l = value_str.split ( ', ' )
+ return [ e for e in l if e.strip() ]
+
+ if not 'field_def' in self.parser:
+ return None
+
+ fdef = descriptionfields.DescriptionFields ()
+
+ for field_name in self.parser ['field_def'].sections():
+ field = descriptionfields.DescriptionField ( field_name )
+ for option, value in self.parser ['field_def'].items ( field_name, 1 ):
+
+ if option == 'alias' or option == 'alias_withcase':
+ for alias in get_list ( value ):
+ field.add_simple_alias ( alias, True )
+
+ elif option == 'alias_nocase':
+ for alias in get_list ( value ):
+ field.add_simple_alias ( alias, False )
+
+ elif option == 'default_value':
+ field.set_default_value ( value )
+
+ elif option == 'allowed_value':
+ field.add_allowed_value ( value )
+
+ elif option == 'allowed_values':
+ for item in get_list ( value ):
+ field.add_allowed_value ( item )
+
+ elif option == 'flags':
+ for flag in get_list ( value ):
+ field.add_flag ( flag )
+ else:
+ # treat option as flag
+ field.add_flag ( option )
+
+ fdef.add ( field )
+
+ return fdef
+ # --- end of _make_field_definition (...) ---
diff --git a/roverlay/const.py b/roverlay/const.py
new file mode 100644
index 0000000..32630cf
--- /dev/null
+++ b/roverlay/const.py
@@ -0,0 +1,51 @@
+# R Overlay -- constants
+# Copyright 2006-2012 Gentoo Foundation
+# Distributed under the terms of the GNU General Public License v2
+
+import copy
+import time
+
+_CONSTANTS = dict (
+ DESCRIPTION = dict (
+ field_separator = ':',
+ comment_char = '#',
+ list_split_regex = '\s*[,;]{1}\s*',
+ file_name = 'DESCRIPTION',
+ ),
+ R_PACKAGE = dict (
+ suffix_regex = '[.](tgz|tbz2|tar|(tar[.](gz|bz2)))',
+ name_ver_separator = '_',
+ ),
+ EBUILD = dict (
+ indent = '\t',
+ default_header = [ '# Copyright 1999-' + str ( time.gmtime() [0] ) + ' Gentoo Foundation',
+ '# Distributed under the terms of the GNU General Public License v2',
+ '# $Header: $',
+ '',
+ 'EAPI=4',
+ '',
+ 'inherit R-packages'
+ ],
+ )
+)
+
+def lookup ( key, fallback_value=None ):
+ path = key.split ( '.' )
+ path.reverse ()
+
+ const_position = _CONSTANTS
+
+ while len ( path ) and const_position:
+ next_key = path.pop ()
+ if next_key in const_position:
+ const_position = const_position [next_key]
+ else:
+ const_position = None
+
+ if const_position:
+ return const_position
+ else:
+ return fallback_value
+
+def clone ( ):
+ return copy.deepcopy ( _CONSTANTS )
diff --git a/roverlay/descriptionfields.py b/roverlay/descriptionfields.py
index 9c028c2..72175cb 100644
--- a/roverlay/descriptionfields.py
+++ b/roverlay/descriptionfields.py
@@ -2,39 +2,95 @@
# Copyright 2006-2012 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
-
-# split from tmpconst / fileio to make configuration possible, but TODO
-
class DescriptionField:
+ """Configuration for a field in the R package description file."""
def __init__ ( self, name ):
+ """Initializes a DescriptionField with a valid(!) name.
+
+ arguments:
+ * name -- name of the field, has to be True (neither empty nor None)
+
+ raises: Exception if name not valid
+ """
+
if not name:
raise Exception ( "description field name is empty." )
self.name = name
+ # --- end of __init__ (...) ---
def get_name ( self ):
+ """Returns the name of this DescriptionField."""
return self.name
- def add_flag ( self, flag, lowercase=True ):
- if not hasattr ( self, flags ):
+ # --- end of get_name (...) ---
+
+
+ def add_flag ( self, flag ):
+ """Adds a flag to this DescriptionField. Flags are always stored in
+ their lowercase form.
+
+ arguments:
+ * flag -- name of the flag
+ """
+ if not hasattr ( self, 'flags' ):
self.flags = set ()
- self.flags.add ( flag, flag.lower() if lowercase else flag )
+ self.flags.add ( flag.lower() )
return None
+ # --- end of add_flag (...) ---
+
+
+ def add_allowed_value ( self, value ):
+ """Adds an allowed value to this DescriptionField, which creates a
+ value whitelist for it. You can later check if a value is allowed using
+ value_allowed (<value> [, <case insensitive?>]).
+
+ arguments:
+ * value -- allowed value
+ """
+
+ if not hasattr ( self, 'allowed_values' ):
+ self.allowed_values = set ()
+
+ self.allowed_values.add ( value )
+
+ return None
+
+ # --- end of add_allowed_value (...) ---
+
def del_flag ( self, flag ):
- if hasattr ( self, flags ):
- self.flags.discard ( flag )
+ """Removes a flag from this DescriptionField. Does nothing if the flag
+ does not exist.
+ """
+ if hasattr ( self, 'flags' ):
+ self.flags.discard ( flag.lower() )
return None
+ # --- end of del_flag (...) ---
+
def add_alias ( self, alias, alias_type='withcase' ):
- if not hasattr ( self, aliases ):
+ """Adds an alias for this DescriptionField's name. This can also be used
+ to combine different fields ('Description' and 'Title') or to fix
+ typos ('Depend' -> 'Depends').
+
+ arguments:
+ * alias -- alias name
+ * alias_type -- type of the alias; currently this is limited to
+ 'withcase' : alias is case sensitive,
+ 'nocase' : alias is case insensitive
+ any other type leads to an error
+
+ raises: KeyError if alias_type unknown.
+ """
+ if not hasattr ( self, 'aliases' ):
self.aliases = dict ()
to_add = dict (
@@ -50,37 +106,83 @@ class DescriptionField:
return None
+ # --- end of add_alias (...) ---
def add_simple_alias ( self, alias, withcase=True ):
- if withcase:
- return self.add_alias ( alias, alias_type='withcase' )
- else:
- return self.add_alias ( alias, alias_type='nocase' )
+ """Adds an alias to this DescriptionField. Its type is either withcase
+ or nocase. See add_alias (...) for details.
+
+ arguments:
+ alias --
+ withcase -- if True (the default): alias_type is withcase, else nocase
+
+ raises: KeyError (passed from add_alias (...))
+ """
+ return self.add_alias ( alias, ( 'withcase' if withcase else 'nocase' ) )
+ # --- end of add_simple_alias (...) ---
def get_default_value ( self ):
- if hasattr ( self, 'default_value' ):
- return self.default_value
- else:
- return None
+ """Returns the default value for this DescriptionField if it exists,
+ else None.
+ """
+ return self.default_value if hasattr ( self, 'default_value' ) else None
+ # --- end of get_default_value (...) ---
+
+
+ def set_default_value ( self, value ):
+ """Sets the default value for this this DescriptionField.
+
+ arguments:
+ * value -- new default value
+ """
+ self.default_value = value
+
+ # --- end of set_default_value (...) ---
+
+
+ def get_flags ( self ):
+ """Returns the flags of this DescriptionField or an empty list (=no flags)."""
+ return self.flags if hasattr ( self, 'flags' ) else []
+
+ # --- end of get_flags (...) ---
+
+
+ def get_allowed_values ( self ):
+ """Returns the allowed values of this DescriptionField or an empty list,
+ which should be interpreted as 'no value restriction'.
+ """
+ return self.allowed_values if hasattr ( self, 'allowed_values' ) else []
+
+ # --- end of get_allowed_values (...) ---
- def get ( self, key, fallback_value=None ):
- if hasattr ( self, key ):
- return self.key
- else:
- return fallback_value
def matches ( self, field_identifier ):
+ """Returns whether field_identifier equals the name of this DescriptionField.
+
+ arguments:
+ * field_identifier --
+ """
return bool ( self.name == field_identifier ) if field_identifier else False
+ # --- end of matches (...) ---
+
+
def matches_alias ( self, field_identifier ):
+ """Returns whether field_identifier equals any alias of this DescriptionField.
+
+ arguments:
+ * field_identifier --
+ """
if not field_identifier:
+ # bad identifier
return False
- if not hasattr ( self, aliases ):
+ elif not hasattr ( self, aliases ):
+ # no aliases
return False
if 'withcase' in self.aliases:
@@ -92,30 +194,211 @@ class DescriptionField:
if field_id_lower in self.aliases ['nocase']:
return True
- def has_flag ( self, flag, lowercase=True ):
- if not hasattr ( self, flags ):
+ # --- end of matches_alias (...) ---
+
+
+ def has_flag ( self, flag ):
+ """Returns whether this DescriptionField has the given flag.
+
+ arguments:
+ * flag --
+ """
+ if not hasattr ( self, 'flags' ):
return False
- return bool ( (flag.lower() if lowercase else flag) in self.flags )
+ return bool ( flag.lower() in self.flags )
+
+ def value_allowed ( self, value, nocase=True ):
+ """Returns whether value is allowed for this DescriptionField.
+
+ arguments:
+ * value -- value to check
+ * nocase -- if True (the default): be case insensitive
+ """
+ allowed_values = self.get_allowed_values ()
+
+ if not allowed_values:
+ return True
+ elif nocase:
+ lowval = value.lower()
+ for allowed in allowed_values:
+ if allowed.lower() == lowval:
+ return True
+
+ else:
+ return bool ( value in allowed_values )
+
+ return False
+
+ # --- end of has_flag (...) ---
+
+# --- end of DescriptionField ---
+
class DescriptionFields:
+ """DescriptionFields stores several instances of DescriptionField and provides
+ 'search in all' methods such as get_fields_with_flag (<flag>).
+ """
def __init__ ( self ):
- fields = dict ()
+ """Initializes an DescriptionFields object."""
+ self.fields = dict ()
+ # result 'caches'
+ ## flag -> [<fields>]
+ self._fields_by_flag = None
+ ## option -> [<fields>]
+ self._fields_by_option = None
+
+ # --- end of __init__ (...) ---
+
def add ( self, desc_field ):
+ """Adds an DescriptionField. Returns 1 desc_field was a DescriptionField
+ and has been added as obj ref, 2 if a new DescriptionField with
+ name=desc_field has been created and added and 0 if this was not
+ possible.
+
+ arguments:
+ * desc_field -- this can either be a DescriptionField or a name.
+ """
if desc_field:
if isinstance ( desc_field, DescriptionField ):
- fields [desc_field.get_name()] = desc_field
+ self.fields [desc_field.get_name()] = desc_field
return 1
elif isinstance ( desc_field, str ):
- fields [desc_field] = DescriptionField ( desc_field )
+ self.fields [desc_field] = DescriptionField ( desc_field )
return 2
return 0
+ # --- end of add (...) ---
+
+
def get ( self, field_name ):
+ """Returns the DescriptionField to which field_name belongs to.
+ This method does, unlike others in DescriptionFields, return a
+ reference to the matching DescriptionField object, not the field name!
+ Returns None if field_name not found.
+
+ arguments:
+ * field_name --
+ """
+
return self.fields [field_name] if field_name in self.fields else None
- # ... TODO
+ # --- end of get (...) ---
+
+
+ def find_field ( self, field_name ):
+ """Determines the name of the DescriptionField to which field_name belongs
+ to. Returns the name of the matching field or None.
+
+ arguments:
+ * field_name --
+ """
+
+ field = get ( field_name )
+ if field is None:
+ for field in self.fields:
+ if field.matches_alias ( field_name ):
+ return field.get_name ()
+ else:
+ return field.get_name ()
+
+ # --- end of find_field (...) ---
+
+
+ def _field_search ( self ):
+ """Scans all stored DescriptionField(s) and creates fast-accessible
+ data to be used in get_fields_with_<sth> (...).
+ """
+ flagmap = dict ()
+ optionmap = dict (
+ defaults = dict (),
+ allowed_values = set ()
+ )
+
+ for field_name in self.fields.keys():
+
+ d = self.fields [field_name].get_default_value()
+ if not d is None:
+ optionmap ['defaults'] [field_name] = d
+
+ if self.fields [field_name].get_allowed_values():
+ optionmap ['allowed_values'].add ( field_name )
+
+ for flag in self.fields [field_name].get_flags():
+ if not flag in flagmap:
+ flagmap [flag] = set ()
+ flagmap [flag].add ( field_name )
+
+ self._fields_by_flag = flagmap
+ self._fields_by_option = optionmap
+ return None
+
+ # --- end of _field_search (...) ---
+
+
+ def get_fields_with_flag ( self, flag, force_update=False ):
+ """Returns the names of the fields that have the given flag.
+
+ arguments:
+ * flag --
+ * force_update -- force recreation of data
+ """
+ if force_update or self._fields_by_flag is None:
+ self._field_search ()
+
+ flag = flag.lower()
+
+ if flag in self._fields_by_flag:
+ return self._fields_by_flag [flag]
+ else:
+ return []
+
+ # --- end of get_fields_with_flag (...) ---
+
+
+ def get_fields_with_option ( self, option, force_update=False ):
+ """Returns a struct with fields that have the given option. The actual
+ data type depends on the requested option.
+
+ arguments:
+ * option --
+ * force_update -- force recreation of data
+ """
+ if force_update or self._fields_by_option is None:
+ self._field_search ()
+
+ if option in self._fields_by_option:
+ return self._fields_by_option [option]
+ else:
+ return []
+
+ # --- end of get_field_with_option (...) ---
+
+
+ def get_fields_with_default_value ( self, force_update=False ):
+ """Returns a dict { '<field name>' -> '<default value>' } for all
+ fields that have a default value.
+
+ arguments:
+ * force_update -- force recreation of data
+ """
+ return self.get_fields_with_option ( 'defaults', force_update )
+
+ # --- end of get_fields_with_default_value (...) ---
+
+
+ def get_fields_with_allowed_values ( self, force_update=False ):
+ """Returns a set { <field name> } for all fields that allow only
+ certain values.
+
+ arguments:
+ * force_update -- force recreation of data
+ """
+ return self.get_fields_with_option ( 'allowed_values', force_update )
+
+ # --- end of get_fields_with_allowed_values (...) ---
+# --- end of DescriptionFields ---
diff --git a/roverlay/fileio.py b/roverlay/descriptionreader.py
similarity index 54%
rename from roverlay/fileio.py
rename to roverlay/descriptionreader.py
index 5c01527..2af4372 100644
--- a/roverlay/fileio.py
+++ b/roverlay/descriptionreader.py
@@ -1,4 +1,4 @@
-# R Overlay -- file in/out
+# R Overlay -- description reader
# Copyright 2006-2012 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
@@ -7,21 +7,25 @@ import tarfile
import logging
import os.path
-
-# temporary import until config and real constants are implemented
-from roverlay import tmpconst as const
+from roverlay import config
+from roverlay import descriptionfields
class DescriptionReader:
"""Description Reader"""
LOGGER = logging.getLogger ( 'DescriptionReader' )
+
def __init__ ( self, package_file, read_now=False ):
"""Initializes a DESCRIPTION file reader."""
- self.fileinfo = self.make_fileinfo ( package_file )
- self.logger = DescriptionReader.LOGGER.getChild ( self.get_log_name() )
- self.desc_data = None
+ if not config.access().get_field_definition():
+ raise Exception ( "Field definition is missing, cannot initialize DescriptionReader." )
+
+ self.field_definition = config.access().get_field_definition()
+ self.fileinfo = self.make_fileinfo ( package_file )
+ self.logger = DescriptionReader.LOGGER.getChild ( self.get_log_name() )
+ self.desc_data = None
if read_now:
@@ -30,10 +34,13 @@ class DescriptionReader:
# --- end of __init__ (...) ---
def get_log_name ( self ):
+ """Returns a logging name that can be used in other modules."""
try:
return self.fileinfo ['filename']
except Exception as any_exception:
return '__undef__'
+ # --- end of get_log_name (...) ---
+
def get_desc ( self, run_if_unset=True ):
if self.desc_data is None:
@@ -58,10 +65,11 @@ class DescriptionReader:
package_file = os.path.basename ( filepath )
- filename = re.sub ( const.RPACKAGE_SUFFIX_REGEX + '$', '', package_file )
+ filename = re.sub ( config.get ( 'R_PACKAGE.suffix_regex' ) + '$', '', package_file )
- # todo move that separator to const
- package_name, sepa, package_version = filename.partition ( '_' )
+ package_name, sepa, package_version = filename.partition (
+ config.get ( 'R_PACKAGE.name_ver_separator', '_' )
+ )
if not sepa:
# file name unexpected, tarball extraction will (probably) fail
@@ -78,7 +86,6 @@ class DescriptionReader:
# --- end of make_fileinfo (...) ---
-
def _parse_read_data ( self, read_data ):
"""Verifies and parses/fixes read data.
@@ -86,80 +93,42 @@ class DescriptionReader:
* read_data -- data from file, will be modified
"""
- def get_fields_with_flag ( flag, foce_update=False ):
-
- matching_fields = []
-
- field = None
- for field in const.DESCRIPTION_FIELD_MAP.keys():
- if flag is None:
- matching_fields.append ( field )
-
- elif 'flags' in const.DESCRIPTION_FIELD_MAP [field]:
- if flag in const.DESCRIPTION_FIELD_MAP [field] ['flags']:
- matching_fields.append ( field )
-
- del field
- return matching_fields
-
- # --- end of get_fields_with_flag (...) ---
-
- def value_in_strlist ( _val, _list, case_insensitive=True ):
- """Returns true if value is in the given list."""
- el = None
- if case_insensitive:
- lowval = _val.lower()
- for el in _list:
- if el.lower() == lowval:
- return True
- del lowval
- else:
- for el in _list:
- if el == _val:
- return True
-
- del el
- return False
- # --- end of value_in_strlist (...) ---
-
- field = None
# insert default values
- for field in const.DESCRIPTION_FIELD_MAP.keys():
- if not field in read_data and 'default_value' in const.DESCRIPTION_FIELD_MAP [field]:
- read_data [field] = const.DESCRIPTION_FIELD_MAP [field] ['default_value']
+ default_values = self.field_definition.get_fields_with_default_value()
+ for field_name in default_values.keys():
+ if not field_name in read_data:
+ read_data [field_name] = default_values [field_name]
+
# join values to a single string
- for field in get_fields_with_flag ( 'joinValues' ):
- if field in read_data.keys():
- read_data [field] = ' ' . join ( read_data [field] )
+ for field_name in self.field_definition.get_fields_with_flag ( 'joinValues' ):
+ print ( "?, ".join ( [ field_name, 'join', str ( read_data ) ] ) )
+ if field_name in read_data:
+ read_data [field_name] = ' ' . join ( read_data [field_name] )
# ensure that all mandatory fields are set
- missing_fields = list()
+ missing_fields = set ()
- for field in get_fields_with_flag ( 'mandatory' ):
- if field in read_data:
- if not len (read_data [field]):
- missing_fields.append ( field )
+ for field_name in self.field_definition.get_fields_with_flag ( 'mandatory' ):
+ if field_name in read_data:
+ if read_data [field_name] is None or len ( read_data [field_name] ) < 1:
+ missing_fields.add ( field_name )
+ #else: ok
else:
- missing_fields.append ( field )
-
-
+ missing_fields.add ( field_name )
# check for fields that allow only certain values
- unsuitable_fields = dict()
+ unsuitable_fields = set()
- for field in read_data.keys():
- if 'allowed_values' in const.DESCRIPTION_FIELD_MAP [field]:
- if not value_in_strlist (
- read_data [field],
- const.DESCRIPTION_FIELD_MAP [field] ['allowed_values']
- ):
- unsuitable_fields.append [field] = read_data [field]
-
- del field
+ restricted_fields = self.field_definition.get_fields_with_allowed_values()
+ for field_name in restricted_fields:
+ if field_name in read_data:
+ if not self.field_definition.get ( field_name ).value_allowed ( read_data [field_name] ):
+ unsuitable_fields.add ( field_name )
+ # summarize results
valid = not bool ( len ( missing_fields ) or len ( unsuitable_fields ) )
if not valid:
self.logger.info ( "Cannot use R package" ) # name?
@@ -200,27 +169,6 @@ class DescriptionReader:
multiple values arranged in a list (dep0, dep1 [, depK]*).
"""
- def check_fieldflag ( field, flag_to_check=None ):
- """Checks if the given field has the specified flag and returns a bool.
-
- arguments:
- * field -- name of the field that should be checked
- * flag_to_check -- name of the flag to check; optional, defaults to None
-
- This method acts as 'field has any flags?' if flag_to_check is None (its default value).
- """
-
- if field in const.DESCRIPTION_FIELD_MAP:
- if 'flags' in const.DESCRIPTION_FIELD_MAP [field]:
- if flag_to_check in const.DESCRIPTION_FIELD_MAP [field] ['flags']:
- return True
- elif flag_to_check is None:
- # 'flags' exist, return true
- return True
-
- return False
- # --- end of check_fieldflag (...) ---
-
svalue_str = value_str.strip()
if not svalue_str:
@@ -231,16 +179,17 @@ class DescriptionReader:
# default return if no context given
return [ svalue_str ]
- elif check_fieldflag ( field_context ):
- # value str is not empty and have flags for field_context, check these
-
- if check_fieldflag ( field_context, 'isList' ):
- # split up this list (that is separated by commata and/or semicolons)
- return re.split (const.DESCRIPTION_LIST_SPLIT_REGEX, svalue_str, 0)
+ elif field_context in self.field_definition.get_fields_with_flag ( 'isList' ):
+ # split up this list (that is separated by commata and/or semicolons)
+ return re.split (
+ config.get ( 'DESCRIPTION.list_split_regex' ),
+ svalue_str,
+ 0
+ )
- elif check_fieldflag ( field_context, 'isWhitespaceList' ):
- # split up this list (that is separated whitespace)
- return re.split ( '\s+', svalue_str, 0 )
+ elif field_context in self.field_definition.get_fields_with_flag ( 'isWhitespaceList' ):
+ # split up this list (that is separated whitespace)
+ return re.split ( '\s+', svalue_str, 0 )
# default return
@@ -275,9 +224,12 @@ class DescriptionReader:
# filepath is a tarball, open tar handle + file handle
th = tarfile.open ( filepath, 'r' )
if pkg_name:
- fh = th.extractfile ( os.path.join ( pkg_name, const.DESCRIPTION_FILE_NAME ) )
+ fh = th.extractfile ( os.path.join (
+ pkg_name,
+ config.get ( 'DESCRIPTION.file_name' )
+ ) )
else:
- fh = th.extractfile ( const.DESCRIPTION_FILE_NAME )
+ fh = th.extractfile ( config.get ( 'DESCRIPTION.file_name' ) )
# have to decode the lines
read = lambda lines : [ line.decode().rstrip() for line in lines ]
@@ -298,60 +250,9 @@ class DescriptionReader:
# --- end of get_desc_from_file (...) ---
- def find_field ( field_identifier ):
- """Determines the real name of a field.
-
- arguments:
- * field_identifier -- name of the field as it appears in the DESCRIPTION file
-
- At first, it is checked whether field_identifier matches the name of
- a field listed in DESCRIPTION_FIELD_MAP (any match results in immediate return).
- Then, a new iteration over the field map compares field_identifier
- with all aliases until the first case-(in)sensitive match (-> immediate return).
- None will be returned if none of the above searches succeed.
-
- In other words: this method decides whether a field_identifier will be used and if so,
- with which name.
- """
-
- # save some time by prevent searching if field_id is empty
- if not field_identifier:
- return None
-
- # search for real field names first
- for field in const.DESCRIPTION_FIELD_MAP.keys():
- if field_identifier == field:
- return field
-
- field_id_lower = field_identifier.lower()
-
- for field in const.DESCRIPTION_FIELD_MAP.keys():
-
- # does extra information (-> alias(es)) for this field exist?
- if 'alias' in const.DESCRIPTION_FIELD_MAP [field]:
-
- if 'withcase' in const.DESCRIPTION_FIELD_MAP [field] ['alias']:
- for alias in const.DESCRIPTION_FIELD_MAP [field] ['alias'] ['withcase']:
- if field_identifier == alias:
- return field
-
- if 'nocase' in const.DESCRIPTION_FIELD_MAP [field] ['alias']:
- for alias in const.DESCRIPTION_FIELD_MAP [field] ['alias'] ['nocase']:
- if field_id_lower == alias.lower():
- return field
-
- #if 'other_alias_type' in const.DESCRIPTION_FIELD_MAP [field] ['alias']:
-
- # returning None if no valid field identifier matches
- return None
-
- # --- end of find_field (...) ---
-
-
self.desc_data = None
read_data = dict ()
-
try:
desc_lines = get_desc_from_file (
self.fileinfo ['filepath'],
@@ -362,14 +263,15 @@ class DescriptionReader:
self.logger.exception ( err )
return self.desc_data
+ field_context = None
- field_context = val = line = sline = None
for line in desc_lines:
+ field_context_ref = None
# using s(tripped)line whenever whitespace doesn't matter
sline = line.lstrip()
- if (not sline) or (line [0] == const.DESCRIPTION_COMMENT_CHAR):
+ if (not sline) or (line [0] == config.get ( 'DESCRIPTION.comment_char' ) ):
# empty line or comment
pass
@@ -389,14 +291,26 @@ class DescriptionReader:
# line introduces a new field context, forget last one
field_context = None
- line_components = sline.partition ( const.DESCRIPTION_FIELD_SEPARATOR )
+ line_components = sline.partition ( config.get ( 'DESCRIPTION.field_separator' ) )
if line_components [1]:
# line contains a field separator, set field context
- field_context = find_field ( line_components [0] )
+ field_context_ref = self.field_definition.get ( line_components [0] )
+
+ if field_context_ref is None:
+ # useless line, skip
+ self.logger.info ( "Skipped a description field: '%s'.", line_components [0] )
+ elif field_context_ref.has_flag ( 'ignore' ):
+ # field ignored
+ self.logger.debug ( "Ignored field '%s'.", field_context )
+
+ else:
+ field_context = field_context_ref.get_name()
+
+ if not field_context:
+ raise Exception ( "Field name is not valid! This should've already been catched in DescriptionField..." )
- if field_context:
- # create a new empty list for field_context
+ # create a new empty list for this field_context
read_data [field_context] = []
# add values to read_data
@@ -404,9 +318,7 @@ class DescriptionReader:
for val in make_values ( line_components [2], field_context ):
read_data [field_context] . append ( val )
- else:
- # useless line, skip
- self.logger.info ( "Skipped a description field: '%s'.", line_components [0] )
+
else:
# reaching this branch means that
@@ -415,11 +327,7 @@ class DescriptionReader:
# this should not occur in description files (bad syntax?)
self.logger.warning ( "Unexpected line in description file: '%s'.", line_components [0] )
-
- del line_components
-
- del sline, line, val, field_context
-
+ # -- end for --
if self._parse_read_data ( read_data ):
self.logger.debug ( "Successfully read file '%s' with data = %s.",
@@ -430,4 +338,4 @@ class DescriptionReader:
# get_desc() is preferred, but this method returns the desc data, too
return self.desc_data
- # --- end of readfile (...) ---
+ # --- end of run (...) ---
diff --git a/roverlay/ebuild.py b/roverlay/ebuild.py
index 1634ef3..4493bb7 100644
--- a/roverlay/ebuild.py
+++ b/roverlay/ebuild.py
@@ -2,9 +2,10 @@
# Copyright 2006-2012 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
+import roverlay.config
+
class Ebuild:
- # could move this to const
- EBUILD_INDENT = "\t"
+ EBUILD_INDENT = roverlay.config.get ( 'EBUILD.indent', '\t' )
ADD_REMAP = {
# pkg vs package
diff --git a/roverlay/ebuildjob.py b/roverlay/ebuildjob.py
index 0357e77..b6c9456 100644
--- a/roverlay/ebuildjob.py
+++ b/roverlay/ebuildjob.py
@@ -2,16 +2,18 @@
# Copyright 2006-2012 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
-import time
import logging
import re
-from roverlay.fileio import DescriptionReader
+from roverlay.descriptionreader import DescriptionReader
from roverlay.ebuild import Ebuild
+from roverlay import config
class EbuildJob:
LOGGER = logging.getLogger ( 'EbuildJob' )
+ DEFAULT_EBUILD_HEADER = config.get ( 'EBUILD.default_header' )
+
# move this to const / config
DEPENDENCY_FIELDS = {
'R_SUGGESTS' : [ 'Suggests' ],
@@ -142,14 +144,7 @@ class EbuildJob:
## default ebuild header, could use some const here (eclass name,..)
ebuild.add ( 'ebuild_header',
- [ '# Copyright 1999-' + str ( time.gmtime() [0] ) + ' Gentoo Foundation',
- '# Distributed under the terms of the GNU General Public License v2',
- '# $Header: $',
- '',
- 'EAPI=4',
- '',
- 'inherit R-packages'
- ],
+ EbuildJob.DEFAULT_EBUILD_HEADER,
False
)
diff --git a/roverlay/tmpconst.py b/roverlay/tmpconst.py
deleted file mode 100644
index 8519aad..0000000
--- a/roverlay/tmpconst.py
+++ /dev/null
@@ -1,108 +0,0 @@
-# R overlay -- constants (temporary file)
-# Copyright 2006-2012 Gentoo Foundation
-# Distributed under the terms of the GNU General Public License v2
-
-# matches .tgz .tbz2 .tar .tar.gz .tar.bz2
-RPACKAGE_SUFFIX_REGEX = '[.](tgz|tbz2|tar|(tar[.](gz|bz2)))'
-
-PACKAGE_CATEGORY = 'sci-R'
-
-DESCRIPTION_FIELD_SEPARATOR = ':'
-
-DESCRIPTION_COMMENT_CHAR = '#'
-
-DESCRIPTION_LIST_SPLIT_REGEX = '\s*[,;]{1}\s*'
-
-DESCRIPTION_FILE_NAME = 'DESCRIPTION'
-
-# moved to <field> -> 'allowed_values'
-##DESCRIPTION_VALID_OS_TYPES = [ "unix" ]
-
-
-# note for 2012-05-25: make this struct more organized, assign real values
-"""The map of used fields in the DESCRIPTION file
-
- stores the real field name as well as field flags and aliases
- that can be case-sensitive (withcase) or not (nocase)
-
- access to these values is
- * for aliases
- DESCRIPTION_FIELD_MAP [<field name>] [alias] [case sensitive ? withcase : nocase] [<index>]
-
- * for flags
- DESCRIPTION_FIELD_MAP [<field name>] [flags] [<index>]
-
- * default values
- DESCRIPTION_FIELD_MAP [<field name>] [default_value]
-
- notable flags:
- * isList : indicates that this field has several values that are
- separated by commata/semicolons =:<DESCRIPTION_LIST_SPLIT_REGEX>
- this disables isWhitespaceList
-
- * isWhitespaceList : indicates that this field has several values separated
- by whitespace
-
- * joinValues : indicates that the values of this field should be concatenated
- after reading them (with a ' ' as separator)
- (this implies that the read values are one string)
-
- * mandatory : cannot proceed if a file does not contain this field (implies ignoring default values)
-
-"""
-
-DESCRIPTION_FIELD_MAP = {
- 'Description' : {
- 'flags' : [ 'joinValues' ],
- },
- 'Title' : {
- 'flags' : [ 'joinValues' ],
- },
- 'Package' : {
- 'flags' : [ 'joinValues' ],
- },
- 'License' : {
- 'flags' : [ 'isList' ],
- },
- 'Version' : {
- 'flags' : [ 'mandatory', 'joinValues' ]
- },
- 'Suggests' : {
- 'alias' : {
- 'nocase' : [ 'Suggests', 'Suggest',
- '%Suggests', 'Suggets', 'Recommends' ]
- },
- },
- 'Depends' : {
- 'alias' : {
- 'nocase' : [ 'Depends', 'Dependencies', 'Dependes',
- '%Depends', 'Depents', 'Require', 'Requires' ],
- },
- 'flags' : [ 'isList' ],
- 'default_value' : '',
- },
- 'Imports' : {
- 'alias' : {
- 'nocase' : [ 'Imports', 'Import' ]
- },
- },
- 'LinkingTo' : {
- 'alias' : {
- 'nocase' : [ 'LinkingTo', 'LinkingdTo' ]
- },
- },
- 'SystemRequirements' : {
- 'alias' : {
- 'nocase' : [ 'SystemRequirements', 'SystemRequirement' ]
- },
- },
- 'OS_Type' : {
- 'alias' : {
- 'nocase' : [ 'OS_TYPE' ]
- },
- 'allowed_values' : [ 'unix' ],
- },
- 'test-default' : {
- 'default_value' : 'some default value'
- }
-}
next reply other threads:[~2012-06-01 15:47 UTC|newest]
Thread overview: 159+ messages / expand[flat|nested] mbox.gz Atom feed top
2012-06-01 15:46 André Erdmann [this message]
-- strict thread matches above, loose matches on Subject: below --
2015-01-26 17:41 [gentoo-commits] proj/R_overlay:master commit in: roverlay/ André Erdmann
2015-01-26 17:41 André Erdmann
2014-07-18 16:20 André Erdmann
2014-07-18 2:50 [gentoo-commits] proj/R_overlay:wip/addition_control " André Erdmann
2014-07-18 16:20 ` [gentoo-commits] proj/R_overlay:master " André Erdmann
2014-07-18 2:28 [gentoo-commits] proj/R_overlay:wip/addition_control " André Erdmann
2014-07-18 16:20 ` [gentoo-commits] proj/R_overlay:master " André Erdmann
2014-07-16 15:14 André Erdmann
2014-06-05 22:09 André Erdmann
2014-04-01 16:38 André Erdmann
2014-02-16 16:30 André Erdmann
2014-02-15 19:49 André Erdmann
2014-02-15 19:49 André Erdmann
2014-01-25 18:14 André Erdmann
2013-12-11 18:40 André Erdmann
2013-12-11 18:40 André Erdmann
2013-09-23 15:30 André Erdmann
2013-09-20 15:57 André Erdmann
2013-09-19 15:00 André Erdmann
2013-09-17 16:40 André Erdmann
2013-09-17 16:40 André Erdmann
2013-09-17 16:40 André Erdmann
2013-09-17 16:40 André Erdmann
2013-09-16 13:43 André Erdmann
2013-09-13 15:10 André Erdmann
2013-09-12 16:36 André Erdmann
2013-09-12 16:36 André Erdmann
2013-09-12 16:36 André Erdmann
2013-09-11 11:14 André Erdmann
2013-09-11 10:19 André Erdmann
2013-09-10 14:40 André Erdmann
2013-09-10 14:40 André Erdmann
2013-09-10 14:40 André Erdmann
2013-09-10 14:40 André Erdmann
2013-09-06 17:27 André Erdmann
2013-09-06 17:27 André Erdmann
2013-09-03 15:50 André Erdmann
2013-09-02 12:27 André Erdmann
2013-09-02 8:44 André Erdmann
2013-08-30 14:49 André Erdmann
2013-08-30 14:49 André Erdmann
2013-08-29 12:36 André Erdmann
2013-08-29 12:36 André Erdmann
2013-08-28 15:54 André Erdmann
2013-08-27 15:39 André Erdmann
2013-08-23 13:52 André Erdmann
2013-08-23 13:52 André Erdmann
2013-08-23 13:52 André Erdmann
2013-08-19 15:42 André Erdmann
2013-08-16 14:05 André Erdmann
2013-08-16 11:02 André Erdmann
2013-08-16 10:43 André Erdmann
2013-08-16 10:43 André Erdmann
2013-08-14 14:56 André Erdmann
2013-08-14 14:56 André Erdmann
2013-08-13 8:56 André Erdmann
2013-08-13 8:56 André Erdmann
2013-08-13 8:56 André Erdmann
2013-08-12 8:28 André Erdmann
2013-08-12 8:18 André Erdmann
2013-08-07 16:10 André Erdmann
2013-08-02 14:30 André Erdmann
2013-08-02 10:34 André Erdmann
2013-08-02 10:34 André Erdmann
2013-08-01 12:44 André Erdmann
2013-08-01 12:44 André Erdmann
2013-07-29 14:56 André Erdmann
2013-07-29 8:55 André Erdmann
2013-07-26 13:02 André Erdmann
2013-07-23 7:51 André Erdmann
2013-07-23 7:51 André Erdmann
2013-07-19 18:00 [gentoo-commits] proj/R_overlay:gsoc13/next " André Erdmann
2013-07-23 7:51 ` [gentoo-commits] proj/R_overlay:master " André Erdmann
2013-07-17 18:05 [gentoo-commits] proj/R_overlay:gsoc13/next " André Erdmann
2013-07-17 18:05 ` [gentoo-commits] proj/R_overlay:master " André Erdmann
2013-07-15 22:31 [gentoo-commits] proj/R_overlay:gsoc13/next " André Erdmann
2013-07-16 16:36 ` [gentoo-commits] proj/R_overlay:master " André Erdmann
2013-07-12 13:57 André Erdmann
2013-06-22 15:24 André Erdmann
2013-06-22 15:24 André Erdmann
2013-06-22 15:24 André Erdmann
2013-06-22 15:24 André Erdmann
2013-06-19 18:58 [gentoo-commits] proj/R_overlay:gsoc13/next " André Erdmann
2013-06-22 15:24 ` [gentoo-commits] proj/R_overlay:master " André Erdmann
2013-06-19 18:58 [gentoo-commits] proj/R_overlay:gsoc13/next " André Erdmann
2013-06-19 18:59 ` [gentoo-commits] proj/R_overlay:master " André Erdmann
2013-06-13 16:34 André Erdmann
2013-06-05 18:08 [gentoo-commits] proj/R_overlay:gsoc13/next " André Erdmann
2013-06-13 16:34 ` [gentoo-commits] proj/R_overlay:master " André Erdmann
2013-06-05 18:08 [gentoo-commits] proj/R_overlay:gsoc13/next " André Erdmann
2013-06-13 16:34 ` [gentoo-commits] proj/R_overlay:master " André Erdmann
2013-06-04 21:06 André Erdmann
2013-04-25 16:44 André Erdmann
2013-04-25 16:44 André Erdmann
2013-03-05 11:27 André Erdmann
2013-02-09 20:45 André Erdmann
2013-02-05 17:48 André Erdmann
2013-02-05 17:48 André Erdmann
2013-01-30 20:16 André Erdmann
2013-01-30 20:16 André Erdmann
2013-01-28 23:54 André Erdmann
2013-01-28 23:54 André Erdmann
2013-01-28 23:54 André Erdmann
2012-10-02 10:04 André Erdmann
2012-08-20 11:16 André Erdmann
2012-08-13 18:07 André Erdmann
2012-08-09 9:26 André Erdmann
2012-08-08 23:46 André Erdmann
2012-08-08 23:46 André Erdmann
2012-08-07 8:50 André Erdmann
2012-08-02 15:14 André Erdmann
2012-08-01 7:25 André Erdmann
2012-07-31 17:51 André Erdmann
2012-07-30 8:52 André Erdmann
2012-07-30 8:52 André Erdmann
2012-07-24 16:59 [gentoo-commits] proj/R_overlay:overlay_wip " André Erdmann
2012-07-30 8:52 ` [gentoo-commits] proj/R_overlay:master " André Erdmann
2012-07-18 16:49 [gentoo-commits] proj/R_overlay:overlay_wip " André Erdmann
2012-07-30 8:52 ` [gentoo-commits] proj/R_overlay:master " André Erdmann
2012-07-16 16:15 André Erdmann
2012-07-16 16:15 André Erdmann
2012-07-16 16:15 [gentoo-commits] proj/R_overlay:depres_wip " André Erdmann
2012-07-16 16:15 ` [gentoo-commits] proj/R_overlay:master " André Erdmann
2012-07-10 17:43 André Erdmann
2012-07-09 17:19 André Erdmann
2012-07-04 18:21 André Erdmann
2012-07-04 18:21 André Erdmann
2012-07-03 17:48 André Erdmann
2012-06-28 13:29 André Erdmann
2012-06-26 15:42 André Erdmann
2012-06-25 18:19 André Erdmann
2012-06-21 16:55 André Erdmann
2012-06-20 19:03 André Erdmann
2012-06-20 19:03 André Erdmann
2012-06-18 16:27 André Erdmann
2012-06-15 20:34 André Erdmann
2012-06-15 20:34 André Erdmann
2012-06-15 20:34 André Erdmann
2012-06-15 20:34 André Erdmann
2012-06-15 20:34 André Erdmann
2012-06-15 20:34 André Erdmann
2012-06-12 17:17 André Erdmann
2012-06-06 19:52 André Erdmann
2012-06-06 19:52 André Erdmann
2012-06-06 19:52 André Erdmann
2012-06-05 17:30 André Erdmann
2012-06-04 19:07 André Erdmann
2012-06-04 19:07 André Erdmann
2012-06-04 15:43 André Erdmann
2012-06-01 16:19 André Erdmann
2012-06-01 16:19 André Erdmann
2012-05-31 18:24 André Erdmann
2012-05-30 20:15 André Erdmann
2012-05-30 19:36 André Erdmann
2012-05-30 19:36 André Erdmann
2012-05-30 16:09 André Erdmann
2012-05-30 16:09 André Erdmann
2012-05-30 16:09 André Erdmann
2012-05-30 16:09 André Erdmann
2012-05-30 10:58 André Erdmann
2012-05-30 10:58 André Erdmann
2012-05-30 10:58 André Erdmann
2012-05-30 10:58 André Erdmann
2012-05-29 17:09 André Erdmann
2012-05-29 17:09 André Erdmann
2012-05-29 17:09 André Erdmann
2012-05-29 17:09 André Erdmann
2012-05-29 17:09 André Erdmann
2012-05-26 13:14 André Erdmann
2012-05-26 13:14 André Erdmann
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=1338562569.ac0b226fd7d16dad7a4b8d30b4293fe401406c1c.dywi@gentoo \
--to=dywi@mailerd.de \
--cc=gentoo-commits@lists.gentoo.org \
--cc=gentoo-dev@lists.gentoo.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox