This file is indexed.

/usr/lib/python3/dist-packages/CTDopts/CTDopts.py is in python3-ctdopts 1.2-3.

This file is owned by root:root, with mode 0o644.

The actual contents of the file can be viewed below.

   1
   2
   3
   4
   5
   6
   7
   8
   9
  10
  11
  12
  13
  14
  15
  16
  17
  18
  19
  20
  21
  22
  23
  24
  25
  26
  27
  28
  29
  30
  31
  32
  33
  34
  35
  36
  37
  38
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
import argparse
from collections import OrderedDict, Mapping
from itertools import chain
from xml.etree.ElementTree import Element, SubElement, tostring, parse
from xml.dom.minidom import parseString
import warnings

# dummy classes for input-file and output-file CTD types.


class _ASingleton(type):
    """
    A metaclass for singletons
    """
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(_ASingleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]


class _Null(object, metaclass=_ASingleton):
    """
    A null singleton for non-initialized fields to distinguish between initialized=None and non-initialized members
    """


class _InFile(str):
    """Dummy class for input-file CTD type. I think most users would want to just get the file path
    string but if it's required to open these files for reading or writing, one could do it in these
    classes in a later release. Otherwise, it's equivalent to str with the information that we're
    dealing with a file argument.
    """
    pass


class _OutFile(str):
    """Same thing, a dummy class for output-file CTD type."""
    pass


# module globals for some common operations (python types to CTD-types back and forth)
TYPE_TO_CTDTYPE = {int: 'int', float: 'float', str: 'string', bool: 'boolean',
                   _InFile: 'input-file', _OutFile: 'output-file'}
CTDTYPE_TO_TYPE = {'int': int, 'float': float, 'double': float, 'string': str, 'boolean': bool, 'bool': bool,
                   'input-file': _InFile, 'output-file': _OutFile, int: int, float: float, str: str,
                   bool: bool, _InFile: _InFile, _OutFile: _OutFile}
PARAM_DEFAULTS = {'advanced': False, 'required': False, 'restrictions': None, 'description': None,
                  'supported_formats': None, 'tags': None, 'position': None}  # unused. TODO.
# a boolean type caster to circumvent bool('false')==True when we cast CTD 'value' attributes to their correct type
CAST_BOOLEAN = lambda x: bool(x) if not isinstance(x, str) else (x in ('true', 'True', '1'))
# instead of using None or _Null, we define non-present 'position' attribute values as -1
NO_POSITION = -1


# Module-level functions for querying and manipulating argument dictionaries.
def get_nested_key(arg_dict, key_list):
    """Looks up a nested key in an arbitrarily nested dictionary. `key_list` should be an iterable:

    get_nested_key(args, ['group', 'subgroup', 'param']) returns args['group']['subgroup']['param']
    """
    key_list = [key_list] if isinstance(key_list, str) else key_list  # just to be safe.
    res = arg_dict
    for key in key_list:
        res = res[key]
    else:
        return res


def set_nested_key(arg_dict, key_list, value):
    """Inserts a value into an arbitrarily nested dictionary, creating nested sub-dictionaries on
    the way if needed:

    set_nested_key(args, ['group', 'subgroup', 'param'], value) sets args['group']['subgroup']['param'] = value
    """
    key_list = [key_list] if isinstance(key_list, str) else key_list  # just to be safe.
    res = arg_dict
    for key in key_list[:-1]:
        if key not in res:
            res[key] = {}  # OrderedDict()
        res = res[key]
    else:
        res[key_list[-1]] = value


def flatten_dict(arg_dict, as_string=False):
    """Creates a flattened dictionary out of a nested dictionary. New keys will be tuples, with the
    nesting information. Ie. arg_dict['group']['subgroup']['param1'] will be
    result[('group', 'subgroup', 'param1')] in the flattened dictionary.

    `as_string` joins the nesting levels into a single string with a semicolon, so the same entry
    would be under result['group:subgroup:param1']
    """
    result = {}

    def flattener(subgroup, level):
        # recursive closure that accesses and modifies result dict and registers nested elements
        # as it encounters them
        for key, value in subgroup.items():
            if isinstance(value, Mapping):  # collections.Mapping instead of dict for generality
                flattener(value, level + [key])
            else:
                result[tuple(level + [key])] = value

    flattener(arg_dict, [])
    if as_string:
        return {':'.join(keylist): value for keylist, value in result.items()}
    else:
        return result


def override_args(*arg_dicts):
    """Takes any number of (nested or flat) argument dictionaries and combines them, giving preference
    to the last one if more than one have the same entry. Typically would be used like:

    combined_args = override_args(args_from_ctd, args_from_commandline)
    """
    overridden_args = dict(chain(*(iter(flatten_dict(d).items()) for d in arg_dicts)))
    result = {}
    for keylist, value in overridden_args.items():
        set_nested_key(result, keylist, value)
    return result


def _translate_ctd_to_param(attribs):
    """Translates a CTD <ITEM> or <ITEMLIST> XML-node's attributes to keyword arguments that Parameter's
    constructor expects. One should be able to call Parameter(*result) with the output of this function.
    For list parameters, adding is_list=True and getting <LISTITEM> values is needed after translation,
    as they are not stored as XML attributes.
    """

    # right now value is a required field, but it shouldn't be for required parameters.
    if 'value' in attribs:  # TODO 1_6_3, this line will be deleted.
        attribs['default'] = attribs.pop('value')  # rename 'value' to 'default' (Parameter constructor takes 'default')

    if 'supported_formats' in attribs:  # supported_formats in CTD xml is called file_formats in CTDopts
        attribs['file_formats'] = attribs.pop('supported_formats')  # rename that attribute too

    if 'restrictions' in attribs:  # find out whether restrictions are choices ('this,that') or numeric range ('3:10')
        if ',' in attribs['restrictions']:
            attribs['choices'] = attribs['restrictions'].split(',')
        elif ':' in attribs['restrictions']:
            n_min, n_max = attribs['restrictions'].split(':')
            n_min = None if n_min == '' else n_min
            n_max = None if n_max == '' else n_max
            attribs['num_range'] = (n_min, n_max)
        else:
            # there is nothing we can split with... so we will assume that this is a restriction of one possible
            # value... anyway, the user should be warned about it
            warnings.warn("Restriction [%s] of a single value found for parameter [%s]. \n"
                          "Restrictions should be comma separated value lists or colon separated values to "
                          "indicate numeric ranges (e.g., 'true,false', '0:14', '1:', ':2.8')\n"
                          "Will use a restriction with one possible value of choice." %
                          (attribs['restrictions'], attribs['name']))
            attribs['choices'] = [attribs['restrictions']]

    # TODO: advanced. Should it be stored as a tag, or should we extend Parameter class to have that attribute?
    # what we can do is keep it as a tag in the model, and change Parameter._xml_node() so that if it finds
    # 'advanced' among its tag-list, make it output it as a separate attribute.
    return attribs


class ArgumentError(Exception):
    """Base exception class for argument related problems.
    """
    def __init__(self, parameter):
        self.parameter = parameter
        self.param_name = ':'.join(self.parameter.get_lineage(name_only=True))


class ArgumentMissingError(ArgumentError):
    """Exception for missing required arguments.
    """
    def __init__(self, parameter):
        super(ArgumentMissingError, self).__init__(parameter)

    def __str__(self):
        return 'Required argument %s missing' % self.param_name


class ArgumentTypeError(ArgumentError):
    """Exception for arguments that can't be casted to the type defined in the model.
    """
    def __init__(self, parameter, value):
        super(ArgumentTypeError, self).__init__(parameter)
        self.value = value

    def __str__(self):
        return "Argument %s is of wrong type. Expected: %s, got %s" % (
            self.param_name, TYPE_TO_CTDTYPE[self.parameter.type], self.value)


class ArgumentRestrictionError(ArgumentError):
    """Exception for arguments violating numeric, file format or controlled vocabulary restrictions.
    """
    def __init__(self, parameter, value):
        super(ArgumentRestrictionError, self).__init__(parameter)
        self.value = value

    def __str__(self):
        return 'Argument restrictions for %s failed. Restriction: %s. Value: %s' % (
            self.param_name, self.parameter.restrictions.ctd_restriction_string(), self.value)


class ModelError(Exception):
    """Exception for errors related to CTDModel building
    """
    def __init__(self):
        super(ModelError, self).__init__()


class ModelParsingError(ModelError):
    """Exception for errors related to CTD parsing
    """
    def __init__(self, message):
        super(ModelParsingError, self).__init__()
        self.message = message
        
    def __str__(self):
        return "An error occurred while parsing the CTD file: %s" % self.message
    
    def __repr__(self):
        return str(self)


class UnsupportedTypeError(ModelError):
    """Exception for attempting to use unsupported types in the model
    """
    def __init__(self, wrong_type):
        super(UnsupportedTypeError, self).__init__()
        self.wrong_type = wrong_type

    def __str__(self):
        return 'Unsupported type encountered during model construction: %s' % self.wrong_type


class DefaultError(ModelError):
    def __init__(self, parameter):
        super(DefaultError, self).__init__()
        self.parameter = parameter

    def __str__(self):
        pass


class _Restriction(object):
    """Superclass for restriction classes (numeric, file format, controlled vocabulary).
    """
    def __init__(self):
        pass

    # if Python had virtual methods, this one would have a _single_check() virtual method, as all
    # subclasses have to implement for check() to go through. check() expects them to be present,
    # and validates normal and list parameters accordingly.
    def check(self, value):
        """Checks whether `value` satisfies the restriction conitions. For list parameters it checks
        every element individually.
        """
        if isinstance(value, list):  # check every element of list (in case of list parameters)
            return all((self._single_check(v) for v in value))
        else:
            return self._single_check(value)


class _NumericRange(_Restriction):
    """Class for numeric range restrictions. Stores valid numeric ranges, checks values against
    them and outputs CTD restrictions attribute strings.
    """
    def __init__(self, n_type, n_min=None, n_max=None):
        super(_NumericRange, self).__init__()
        self.n_type = n_type
        self.n_min = self.n_type(n_min) if n_min is not None else None
        self.n_max = self.n_type(n_max) if n_max is not None else None

    def ctd_restriction_string(self):
        n_min = str(self.n_min) if self.n_min is not None else ''
        n_max = str(self.n_max) if self.n_max is not None else ''
        return '%s:%s' % (n_min, n_max)

    def _single_check(self, value):
        if self.n_min is not None and value < self.n_min:
            return False
        elif self.n_max is not None and value > self.n_max:
            return False
        else:
            return True

    def __repr__(self):
        return 'numeric range: %s to %s' % (self.n_min, self.n_max)


class _FileFormat(_Restriction):
    """Class for file format restrictions. Stores valid file formats, checks filenames against them
    and outputs CTD supported_formats attribute strings.
    """
    def __init__(self, formats):
        super(_FileFormat, self).__init__()
        if isinstance(formats, str):  # to handle ['txt', 'csv', 'tsv'] and '*.txt,*.csv,*.tsv'
            formats = [x.replace('*.', '').strip() for x in formats.split(',')]
        self.formats = formats

    def ctd_restriction_string(self):
        return ','.join(('*.' + f for f in self.formats))

    def _single_check(self, value):
        for f in self.formats:
            if value.endswith('.' + f):
                return True
        return False

    def __repr__(self):
        return 'file formats: %s' % (', '.join(self.formats))


class _Choices(_Restriction):
    """Class for controlled vocabulary restrictions. Stores controlled vocabulary elements, checks
    values against them and outputs CTD restrictions attribute strings.
    """
    def __init__(self, choices):
        super(_Choices, self).__init__()
        if isinstance(choices, str):  # If it actually has to run, a user is screwing around...
            choices = choices.replace(', ', ',').split(',')
        self.choices = choices

    def _single_check(self, value):
        return value in self.choices

    def ctd_restriction_string(self):
        return ','.join(self.choices)

    def __repr__(self):
        return 'choices: %s' % (', '.join(map(str, self.choices)))


class Parameter(object):

    def __init__(self, name, parent, **kwargs):
        """Required positional arguments: `name` string and `parent` ParameterGroup object

        Optional keyword arguments:
            `type`: Python type object, or a string of a valid CTD types.
                    For all valid values, see: CTDopts.CTDTYPE_TO_TYPE.keys()
            `default`: default value. Will be casted to the above type (default None)
            `is_list`: bool, indicating whether this is a list parameter (default False)
            `required`: bool, indicating whether this is a required parameter (default False)
            `description`: string containing parameter description (default None)
            `tags`: list of strings or comma separated string (default [])
            `num_range`: (min, max) tuple. None in either position makes it unlimited
            `choices`: list of allowed values (controlled vocabulary)
            `file_formats`: list of allowed file extensions
            `short_name`: string for short name annotation
            `position`: index (1-based) of the position on which the parameter appears on the command-line
        """
        self.name = name
        self.parent = parent
        self.short_name = kwargs.get('short_name', _Null)

        try:
            self.type = CTDTYPE_TO_TYPE[kwargs.get('type', str)]
        except:
            raise UnsupportedTypeError(kwargs.get('type'))

        self.tags = kwargs.get('tags', [])
        if isinstance(self.tags, str):  # so that tags can be passed as ['tag1', 'tag2'] or 'tag1,tag2'
            self.tags = list(filter(bool, self.tags.split(',')))  # so an empty string doesn't produce ['']
        self.required = CAST_BOOLEAN(kwargs.get('required', False))
        self.is_list = CAST_BOOLEAN(kwargs.get('is_list', False))
        self.description = kwargs.get('description', None)
        self.advanced = CAST_BOOLEAN(kwargs.get('advanced', False))
        self.position = int(kwargs.get('position', str(NO_POSITION)))

        default = kwargs.get('default', _Null)

        self._validate_numerical_defaults(default)
                    
        # TODO 1_6_3: right now the CTD schema requires the 'value' attribute to be present for every parameter.
        # So every time we build a model from a CTD file, we find at least a default='' or default=[]
        # for every parameter. This should change soon, but for the time being, we have to get around this
        # and disregard such default attributes. The below two lines will be deleted after fixing 1_6_3.
        if default == '' or (self.is_list and default == []):
            default = _Null

        # enforce that default is the correct type if exists. Elementwise for lists
        if default is _Null:
            self.default = _Null
        elif default is None:
            self.default = None
        else:
            if self.is_list:
                self.default = list(map(self.type, default))
            else:
                self.default = self.type(default)
        # same for choices. I'm starting to think it's really unpythonic and we should trust input. TODO

        if self.type == bool:
            assert self.is_list is False, "Boolean flag can't be a list type"
            self.required = False  # override whatever we found. Boolean flags can't be required...
            self.default = CAST_BOOLEAN(default)

        # Default value should exist IFF argument is not required.
        # TODO: if we can have optional list arguments they don't have to have a default? (empty list)
        # TODO: CTD Params 1.6.3 have a required value attrib. That's very wrong for parameters that are required.
        # ... until that's ironed out, we have to comment this part out.
        #
        # ACTUALLY now that I think of it, letting required fields have value attribs set too
        # can be useful for users who want to abuse CTD and build models from argument-storing CTDs.
        # I know some users will do this (who are not native CTD users just want to convert their stuff
        # with minimal effort) so we might as well let them.
        #
        # if self.required:
        #     assert self.default is None, ('Required field `%s` has default value' % self.name)
        # else:
        #     assert self.default is not None, ('Optional field `%s` has no default value' % self.name)

        self.restrictions = None
        if 'num_range' in kwargs:
            try:
                self.restrictions = _NumericRange(self.type, *kwargs['num_range'])
            except ValueError:
                num_range = kwargs['num_range']
                raise ModelParsingError("Provided range [%s, %s] is not of type %s" %
                                        (num_range[0], num_range[1], self.type))
        elif 'choices' in kwargs:
            self.restrictions = _Choices(list(map(self.type, kwargs['choices'])))
        elif 'file_formats' in kwargs:
            self.restrictions = _FileFormat(kwargs['file_formats'])

    # perform some basic validation on the provided default values...
    # an empty string IS NOT a float/int!        
    def _validate_numerical_defaults(self, default):
        if default is not None and default is not _Null:
            if self.type is int or self.type is float:
                defaults_to_validate = []
                errors_so_far = []
                if self.is_list:
                    # for lists, validate each provided element
                    defaults_to_validate.extend(default)
                else:
                    defaults_to_validate.append(default)
                for default_to_validate in defaults_to_validate:
                    try:
                        if self.type is int:
                            int(default_to_validate)
                        else:
                            float(default_to_validate)
                    except ValueError:
                        errors_so_far.append(default_to_validate)

                if len(errors_so_far) > 0:
                    raise ModelParsingError("Invalid default value(s) provided for parameter %(name)s of type %(type)s:"
                                            " '%(default)s'"
                                            % {"name": self.name,
                                               "type": self.type,
                                               "default": ', '.join(map(str, errors_so_far))})

    def get_lineage(self, name_only=False, short_name=False):
        """Returns a list of zero or more ParameterGroup objects plus this Parameter object at the end,
        ie. the nesting lineage of the Parameter object. With `name_only` setting on, it only returns
        the names of said objects. For top level parameters, it's a list with a single element.
        """
        lineage = []
        i = self
        while i.parent is not None:
            # Exclude ParameterGroup here, since they do not have a short_name attribute (lzimmermann)
            lineage.append(i.short_name if short_name and not isinstance(i, ParameterGroup) else i.name if name_only else i)
            i = i.parent
        lineage.reverse()
        return lineage

    def __repr__(self):
        info = []
        info.append('PARAMETER %s%s' % (self.name, ' (required)' if self.required else ''))
        info.append('  type: %s%s%s' % ('list of ' if self.is_list else '', TYPE_TO_CTDTYPE[self.type],
                                        's' if self.is_list else ''))
        if self.default:
            info.append('  default: %s' % self.default)
        if self.tags:
            info.append('  tags: %s' % ', '.join(self.tags))
        if self.restrictions:
            info.append('  restrictions on %s' % self.restrictions)
        if self.description:
            info.append('  description: %s' % self.description)
        return '\n'.join(info)

    def _xml_node(self, arg_dict=None):
        if arg_dict is not None:  # if we call this function with an argument dict, get value from there
            try:
                value = get_nested_key(arg_dict, self.get_lineage(name_only=True))
            except KeyError:
                value = self.default
        else:  # otherwise take the parameter default
            value = self.default

        # XML attributes to be created (depending on whether they are needed or not):
        # name, value, type, description, tags, restrictions, supported_formats

        attribs = OrderedDict()  # LXML keeps the order, ElemenTree doesn't. We use ElementTree though.
        attribs['name'] = self.name
        if not self.is_list:  # we'll deal with list parameters later, now only normal:
            # TODO: once Param_1_6_3.xsd gets fixed, we won't have to set an empty value='' attrib.
            # but right now value is a required attribute.
            attribs['value'] = '' if value is _Null else str(value)
            if self.type is bool:  # for booleans str(True) returns 'True' but the XS standard is lowercase
                attribs['value'] = 'true' if value else 'false'
        attribs['type'] = TYPE_TO_CTDTYPE[self.type]
        if self.description:
            attribs['description'] = self.description
        if self.tags:
            attribs['tags'] = ','.join(self.tags)

        # Choices and NumericRange restrictions go in the 'restrictions' attrib, FileFormat has
        # its own attribute 'supported_formats' for whatever historic reason.
        if isinstance(self.restrictions, _Choices) or isinstance(self.restrictions, _NumericRange):
            attribs['restrictions'] = self.restrictions.ctd_restriction_string()
        elif isinstance(self.restrictions, _FileFormat):
            attribs['supported_formats'] = self.restrictions.ctd_restriction_string()

        if self.is_list:  # and now list parameters
            top = Element('ITEMLIST', attribs)
            
            # (lzimmermann) I guess _Null has to be exluded here, too
            if value is not None and value is not _Null:
                for d in value:
                    SubElement(top, 'LISTITEM', {'value': str(d)})
            return top
        else:
            return Element('ITEM', attribs)

    def _cli_node(self, parent_name, prefix='--'):
        lineage = self.get_lineage(name_only=True)
        top_node = Element('clielement', {"optionIdentifier": prefix+':'.join(lineage)})
        SubElement(top_node, 'mapping', {"referenceName": parent_name+"."+self.name})
        return top_node

    def is_positional(self):
        return self.position != NO_POSITION


class ParameterGroup(object):
    def __init__(self, name, parent, description=None):
        self.name = name
        self.parent = parent
        self.description = description
        self.parameters = OrderedDict()

    def add(self, name, **kwargs):
        """Registers a parameter in a ParameterGroup. Required: `name` string.

        Optional keyword arguments:
            `type`: Python type object, or a string of a valid CTD types.
                    For all valid values, see: CTDopts.CTDTYPE_TO_TYPE.keys()
            `default`: default value. Will be casted to the above type (default None)
            `is_list`: bool, indicating whether this is a list parameter (default False)
            `required`: bool, indicating whether this is a required parameter (default False)
            `description`: string containing parameter description (default None)
            `tags`: list of strings or comma separated string (default [])
            `num_range`: (min, max) tuple. None in either position makes it unlimited
            `choices`: list of allowed values (controlled vocabulary)
            `short_name`: string for short name annotation
        """
        # TODO assertion if name already exists? It just overrides now, but I'm not sure if allowing this behavior is OK
        self.parameters[name] = Parameter(name, self, **kwargs)
        return self.parameters[name]

    def add_group(self, name, description=None):
        """Registers a child parameter group under a ParameterGroup. Required: `name` string. Optional: `description`
        """
        # TODO assertion if name already exists? It just overrides now, but I'm not sure if allowing this behavior is OK
        self.parameters[name] = ParameterGroup(name, self, description)
        return self.parameters[name]

    def _get_children(self):
        children = []
        for child in self.parameters.values():
            if isinstance(child, Parameter):
                children.append(child)
            elif isinstance(child, ParameterGroup):
                children.extend(child._get_children())
        return children

    def _xml_node(self, arg_dict=None):
        xml_attribs = {'name': self.name}
        if self.description:
            xml_attribs['description'] = self.description

        top = Element('NODE', xml_attribs)
        # TODO: if a Parameter comes after an ParameterGroup, the CTD won't validate. BTW, that should be changed.
        # Of course this should never happen if the argument tree is built properly but it would be
        # nice to take care of it if a user happens to randomly define his arguments and groups.
        # So first we could sort self.parameters (Items first, Groups after them).
        for arg in self.parameters.values():
            top.append(arg._xml_node(arg_dict))
        return top

    def _cli_node(self, parent_name="", prefix='--'):
        """
        Generates a list of clielements of that group
        :param arg_dict: dafualt values for elements
        :return: list of clielements
        """
        for arg in self.parameters.values():
            yield arg._cli_node(parent_name=parent_name+"."+self.name, prefix=prefix)

    def __repr__(self):
        info = []
        info.append('PARAMETER GROUP %s (' % self.name)
        for subparam in self.parameters.values():
            info.append(subparam.__repr__())
        info.append(')')
        return '\n'.join(info)


class Mapping(object):
    def __init__(self, reference_name=None):
        self.reference_name = reference_name


class CLIElement(object):
    def __init__(self, option_identifier=None, mappings=[]):
        self.option_identifier = option_identifier
        self.mappings = mappings


class CLI(object):
    def __init__(self, cli_elements=[]):
        self.cli_elements = cli_elements


class CTDModel(object):
    def __init__(self, name=None, version=None, from_file=None, **kwargs):
        """The parameter model of a tool.

        `name`: name of the tool
        `version`: version of the tool
        `from_file`: create the model from a CTD file at provided path

        Other (self-explanatory) keyword arguments:
        `docurl`, `description`, `manual`, `executableName`, `executablePath`, `category`
        """
        if from_file is not None:
            self._load_from_file(from_file)
        else:
            self.name = name
            self.version = version
            # TODO: check whether optional attributes in kwargs are all allowed or just ignore the rest?
            self.opt_attribs = kwargs  # description, manual, docurl, category (+executable stuff).
            self.parameters = ParameterGroup('1', None, 'Parameters of %s' % self.name)  # openMS legacy, top group named "1"
            self.cli = []

    def _load_from_file(self, filename):
        """Builds a CTDModel from a CTD XML file.
        """
        root = parse(filename).getroot()
        assert root.tag == 'tool', "Invalid CTD file, root is not <tool>"  # TODO: own exception

        self.opt_attribs = {}
        self.cli = []

        for tool_required_attrib in ['name', 'version']:
            assert tool_required_attrib in root.attrib, "CTD tool is missing a %s attribute" % tool_required_attrib
            setattr(self, tool_required_attrib, root.attrib[tool_required_attrib])

        for tool_opt_attrib in ['docurl', 'category']:
            if tool_opt_attrib in root.attrib:
                self.opt_attribs[tool_opt_attrib] = root.attrib[tool_opt_attrib]

        for tool_element in root:
            if tool_element.tag in ['manual', 'description', 'executableName', 'executablePath']:
                                    # ignoring: cli, logs, relocators. cli and relocators might be useful later.
                self.opt_attribs[tool_element.tag] = tool_element.text

            if tool_element.tag == 'cli':
                self._build_cli(tool_element.findall('clielement'))

            if tool_element.tag == 'PARAMETERS':
                # tool_element.attrib['version'] == '1.6.2'  # check whether the schema matches the one CTDOpts uses?
                params_container_node = tool_element.find('NODE')
                # we have to check the case in which the parent node contains 
                # item/itemlist elements AND node element children
                params_container_node_contains_items = params_container_node.find('ITEM') is not None or params_container_node.find('ITEMLIST')                 
                # assert params_container_node.attrib['name'] == self.name
                # check params_container_node's first ITEM child's tool version information again? (OpenMS legacy?)
                params = params_container_node.find('NODE')  # OpenMS legacy again, NODE with name="1" on top
                # check for the case when we have PARAMETERS/NODE/ITEM
                if params is None or params_container_node_contains_items:                    
                    self.parameters = self._build_param_model(params_container_node, base=None)
                else:
                    # OpenMS legacy again, PARAMETERS/NODE/NODE/ITEM
                    self.parameters = self._build_param_model(params, base=None)

    def _build_cli(self, xml_cli_elements):
        for xml_cli_element in xml_cli_elements:
            mappings = []
            for xml_mapping in xml_cli_element.findall('mapping'):
                mappings.append(Mapping(xml_mapping.attrib['referenceName'] if 'referenceName' in xml_mapping.attrib else None))
            self.cli.append(CLIElement(xml_cli_element.attrib['optionIdentifier'] if 'optionIdentifier' in xml_cli_element.attrib else None, mappings))

    def _build_param_model(self, element, base):
        if element.tag == 'NODE':
            validate_contains_keys(element.attrib, ['name'], 'NODE')
            if base is None:  # top level group (<NODE name="1">) has to be created on its own
                current_group = ParameterGroup(element.attrib['name'], base, element.attrib.get('description', ''))
            else:  # other groups can be registered as a subgroup, as they'll always have parent base nodes
                current_group = base.add_group(element.attrib['name'], element.attrib.get('description', ''))
            for child in element:
                self._build_param_model(child, current_group)
            return current_group
        elif element.tag == 'ITEM':
            setup = _translate_ctd_to_param(dict(element.attrib))
            validate_contains_keys(setup, ['name'], 'ITEM')
            base.add(**setup)  # register parameter in model
        elif element.tag == 'ITEMLIST':
            setup = _translate_ctd_to_param(dict(element.attrib))
            setup['default'] = [listitem.attrib['value'] for listitem in element]
            setup['is_list'] = True
            validate_contains_keys(setup, ['name'], 'ITEMLIST')
            base.add(**setup)  # register list parameter in model

    def add(self, name, **kwargs):
        """Registers a top level parameter to the model. Required: `name` string.

        Optional keyword arguments:
            `type`: Python type object, or a string of a valid CTD types.
                    For all valid values, see: CTDopts.CTDTYPE_TO_TYPE.keys()
            `default`: default value. Will be casted to the above type (default None)
            `is_list`: bool, indicating whether this is a list parameter (default False)
            `required`: bool, indicating whether this is a required parameter (default False)
            `description`: string containing parameter description (default None)
            `tags`: list of strings or comma separated string (default [])
            `num_range`: (min, max) tuple. None in either position makes it unlimited
            `choices`: list of allowed values (controlled vocabulary)
            `file_formats`: list of allowed file extensions
            `short_name`: string for short name annotation
        """
        return self.parameters.add(name, **kwargs)

    def add_group(self, name, description=None):
        """Registers a top level parameter group to the model. Required: `name` string. Optional: `description`
        """
        return self.parameters.add_group(name, description)

    def list_parameters(self):
        """Returns a list of all Parameter objects registered in the model.
        """
        # root node will list all its children (recursively, if they are nested in ParameterGroups)
        return self.parameters._get_children()

    def get_defaults(self):
        """Returns a nested dictionary with all parameters of the model having default values.
        """
        params_w_default = (p for p in self.list_parameters() if p.default is not _Null)
        defaults = {}
        for param in params_w_default:
            set_nested_key(defaults, param.get_lineage(name_only=True), param.default)
        return defaults

    def validate_args(self, args_dict, enforce_required=0, enforce_type=0, enforce_restrictions=0):
        """Validates an argument dictionary against the model, and returns a type-casted argument
        dictionary with defaults for missing arguments. Valid values for `enforce_required`,
        `enforce_type` and `enforce_restrictions` are 0, 1 and 2, where the different levels are:
            * 0: doesn't enforce anything,
            * 1: raises a warning
            * 2: raises an exception
        """
        # iterate over model parameters, look them up in the argument dictionary, convert to correct type,
        # use default if argument is not present and raise exception if required argument is missing.
        validated_args = {}  # OrderedDict()
        all_params = self.list_parameters()
        for param in all_params:
            lineage = param.get_lineage(name_only=True)
            try:
                arg = get_nested_key(args_dict, lineage)
                # boolean values are the only ones that don't get casted correctly with, say, bool('false')
                typecast = param.type if param.type is not bool else CAST_BOOLEAN
                try:
                    validated_value = list(map(typecast, arg)) if param.is_list else typecast(arg)
                except ValueError:  # type casting failed
                    validated_value = arg  # just keep it as a string (or list of strings)
                    if enforce_type:  # but raise a warning or exception depending on enforcement level
                        if enforce_type == 1:
                            warnings.warn('Argument %s is of wrong type. Expected %s, got: %s' %
                                          (':'.join(lineage), TYPE_TO_CTDTYPE[param.type], arg))
                        else:
                            raise ArgumentTypeError(param, arg)

                if enforce_restrictions and param.restrictions and not param.restrictions.check(validated_value):
                    if enforce_restrictions == 1:
                        warnings.warn('Argument restrictions for %s violated. Restriction: %s. Value: %s' %
                                      (':'.join(lineage), param.restrictions.ctd_restriction_string(), validated_value))
                    else:
                        raise ArgumentRestrictionError(param, validated_value)

                set_nested_key(validated_args, lineage, validated_value)
            except KeyError:  # argument was not found, checking whether required and using defaults if not
                if param.required:
                    if not enforce_required:
                        continue  # this argument will be missing from the dict as required fields have no default value
                    elif enforce_required == 1:
                        warnings.warn('Required argument %s missing' % ':'.join(lineage), UserWarning)
                    else:
                        raise ArgumentMissingError(param)
                else:
                    set_nested_key(validated_args, lineage, param.default)
        return validated_args

    def parse_cl_args(self, cl_args=None, prefix='--', short_prefix="-", get_remaining=False):
        """Parses command line arguments `cl_args` (either a string or a list like sys.argv[1:])
        assuming that parameter names are prefixed by `prefix` (default '--').

        Returns a nested dictionary with found arguments. Note that parameters have to be registered
        in the model to be parsed and returned.

        Remaining (unmatchable) command line arguments can be accessed if the method is called with
        `get_remaining`. In this case, the method returns a tuple, whose first element is the
        argument dictionary, the second a list of unmatchable command line options.
        """
        cl_parser = argparse.ArgumentParser()
        for param in self.list_parameters():
            lineage = param.get_lineage(name_only=True)
            short_lineage = param.get_lineage(name_only=True, short_name=True)
            cl_arg_kws = {}  # argument processing info passed to argparse in keyword arguments, we build them here
            if param.type is bool:  # boolean flags are not followed by a value, only their presence is required
                cl_arg_kws['action'] = 'store_true'
            else:
                # we take every argument as string and cast them only later in validate_args() if
                # explicitly asked for. This is because we don't want to deal with type exceptions
                # at this stage, and prefer the multi-leveled strictness settings in validate_args()
                cl_arg_kws['type'] = str

            if param.is_list:
                # or '+' rather? Should we allow empty lists here? If default is a proper list with elements
                # that we want to clear, this would be the only way to do it so I'm inclined to use '*'
                cl_arg_kws['nargs'] = '*'

            if param.default is not _Null():
                cl_arg_kws['default'] = param.default

            if param.required:
                cl_arg_kws['required'] = True

            # hardcoded 'group:subgroup:param1'
            if all(a is not _Null for a in short_lineage):
                cl_parser.add_argument(short_prefix+':'.join(short_lineage), prefix + ':'.join(lineage), **cl_arg_kws)
            else:
                cl_parser.add_argument(prefix + ':'.join(lineage), **cl_arg_kws)


        cl_arg_list = cl_args.split() if isinstance(cl_args, str) else cl_args
        #if no arguments are given print help
        if not cl_arg_list:
            cl_arg_list.append("-h")
        parsed_args, rest = cl_parser.parse_known_args(cl_arg_list)
        res_args = {}  # OrderedDict()
        for param_name, value in vars(parsed_args).items():
            # None values are created by argparse if it didn't find the argument or default=None, we skip params
            # that dont have a default value
            if value is not None or value == self.parameters.parameters[param_name].default:
                set_nested_key(res_args, param_name.split(':'), value)
        return res_args if not get_remaining else (res_args, rest)

    def generate_ctd_tree(self, arg_dict=None, log=None, cli=False, prefix='--'):
        """Generates an XML ElementTree from the model and returns the top <tool> Element object,
        that can be output to a file (CTDModel.write_ctd() does everything needed if the user
        doesn't need access to the actual element-tree).
        Calling this function without any arguments generates the tool-describing CTD with default
        values. For parameter-storing and logging optional arguments can be passed:

        `arg_dict`: nested dictionary with values to be used instead of defaults.
        `log`: dictionary with the following optional keys:
            'time_start' and 'time_finish': proper XML date strings (eg. datetime.datetime.now(pytz.utc).isoformat())
            'status': exit status
            'output': standard output or whatever output the user intends to log
            'warning': warning logs
            'error': standard error or whatever error log the user wants to store
        `cli`: boolean whether or not cli elements should be generated (needed for GenericKNIMENode for example)
        """
        tool_attribs = OrderedDict()
        tool_attribs['version'] = self.version
        tool_attribs['name'] = self.name
        tool_attribs['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance"
        tool_attribs['xsi:schemaLocation'] = "https://github.com/genericworkflownodes/CTDopts/raw/master/schemas/CTD_0_3.xsd"

        opt_attribs = ['docurl', 'category']
        for oo in opt_attribs:
            if oo in self.opt_attribs:
                tool_attribs[oo] = self.opt_attribs[oo]

        tool = Element('tool', tool_attribs)  # CTD root

        opt_elements = ['manual', 'description', 'executableName', 'executablePath']

        for oo in opt_elements:
            if oo in self.opt_attribs:
                SubElement(tool, oo).text = self.opt_attribs[oo]

        if log is not None:
            # log is supposed to be a dictionary, with the following keys (none of them being required):
            # time_start, time_finish, status, output, warning, error
            # generate
            log_node = SubElement(tool, 'log')
            if 'time_start' in log:  # expect proper XML date string like datetime.datetime.now(pytz.utc).isoformat()
                log_node.attrib['executionTimeStart'] = log['time_start']
            if 'time_finish' in log:
                log_node.attrib['executionTimeStop'] = log['time_finish']
            if 'status' in log:
                log_node.attrib['executionStatus'] = log['status']
            if 'output' in log:
                SubElement(log_node, 'executionMessage').text = log['output']
            if 'warning' in log:
                SubElement(log_node, 'executionWarning').text = log['warning']
            if 'error' in log:
                SubElement(log_node, 'executionError').text = log['error']

        # XML.ETREE SYNTAX
        params = SubElement(tool, 'PARAMETERS', {
            'version': '1.6.2',
            'xmlns:xsi': "http://www.w3.org/2001/XMLSchema-instance",
            'xsi:noNamespaceSchemaLocation': "https://github.com/genericworkflownodes/CTDopts/raw/master/schemas/Param_1_6_2.xsd"
        })

        # This seems to be some OpenMS hack (defining name, description, version for the second time)
        # but I'll stick to it for consistency
        top_node = SubElement(params, 'NODE', name=self.name, description=self.opt_attribs.get('description', ''))

        SubElement(top_node, 'ITEM',
            name='version',
            value=self.version,
            type='string',
            description='Version of the tool that generated this parameters file.',
            tags='advanced')

        # all the above was boilerplate, now comes the actual parameter tree generation
        args_top_node = self.parameters._xml_node(arg_dict)
        top_node.append(args_top_node)

        if cli:
            cli_node = SubElement(tool, "cli")
            for e in self.parameters._cli_node(parent_name=self.name, prefix=prefix):
                cli_node.append(e)

        # # LXML w/ pretty print syntax
        # return tostring(tool, pretty_print=True, xml_declaration=True, encoding="UTF-8")

        # xml.etree syntax (no pretty print available, so we use xml.dom.minidom stuff)
        return tool

    def write_ctd(self, out_file, arg_dict=None, log=None, cli=False):
        """Generates a CTD XML from the model and writes it to `out_file`, which is either a string
        to a file path or a stream with a write() method.

        Calling this function without any arguments besides `out_file` generates the tool-describing
        CTD with default values. For parameter-storing and logging optional arguments can be passed:

        `arg_dict`: nested dictionary with values to be used instead of defaults.
        `log`: dictionary with the following optional keys:
            'time_start' and 'time_finish': proper XML date strings (eg. datetime.datetime.now(pytz.utc).isoformat())
            'status': exit status
            'output': standard output or whatever output the user intends to log
            'warning': warning logs
            'error': standard error or whatever error log the user wants to store
        `cli`: boolean whether or not cli elements should be generated (needed for GenericKNIMENode for example)
        """
        xml_content = parseString(tostring(self.generate_ctd_tree(arg_dict, log, cli), encoding="UTF-8")).toprettyxml()

        if isinstance(out_file, str):  # if out_file is a string, we create and write the file
            with open(out_file, 'w') as f:
                f.write(xml_content)
        else:  # otherwise we assume it's a writable stream and write into that.
            out_file.write(xml_content)


def args_from_file(filename):
    """Takes a CTD file and returns a nested dictionary with all argument values found. It's not
    linked to a model, so there's no type casting or validation done on the arguments. This is useful
    for users who just want to access arguments in CTD files without having to deal with building a CTD model.

    If type casting or validation is required, two things can be done to hack one's way around it:

    Build a model from the same file and call get_defaults() on it. This takes advantage from the
    fact that when building a model from a CTD, the value attributes are used as defaults. Although
    one shouldn't build a model from an argument storing CTD (as opposed to tool describing CTDs)
    there's no technical obstacle to do so.
    """
    def get_args(element, base=None):
        # recursive argument lookup if encountering <NODE>s
        if element.tag == 'NODE':
            current_group = {}  # OrderedDict()
            for child in element:
                get_args(child, current_group)

            if base is not None:
                base[element.attrib['name']] = current_group
            else:
                # top level <NODE name='1'> is the only one called with base=None.
                # As the argument parsing is recursive, whenever the top node finishes, we are done
                # with the parsing and have to return the results.
                return current_group
        elif element.tag == 'ITEM':
            if 'value' in element.attrib:
                base[element.attrib['name']] = element.attrib['value']
        elif element.tag == 'ITEMLIST':
            if element.getchildren():
                base[element.attrib['name']] = [listitem.attrib['value'] for listitem in element]

    root = parse(filename).getroot()
    param_root = root if root.tag == 'PARAMETERS' else root.find('PARAMETERS')
    parameters = param_root.find('NODE').find('NODE')
    return get_args(parameters, base=None)


def parse_cl_directives(cl_args, write_tool_ctd='write_tool_ctd', write_param_ctd='write_param_ctd',
                       input_ctd='input_ctd', prefix='--'):
    '''Parses command line CTD processing directives. `write_tool_ctd`, `write_param_ctd` and `input_ctd`
    string are customizable, and will be parsed for in command line. `prefix` should be one or two dashes,
    default is '--'.

    Returns a dictionary with keys
        'write_tool_ctd': if flag set, either True or the filename provided in command line. Otherwise None.
        'write_param_ctd': if flag set, either True or the filename provided in command line. Otherwise None.
        'input_ctd': filename if found, otherwise None
    '''
    parser = argparse.ArgumentParser()
    parser.add_argument(prefix + write_tool_ctd, nargs='*')
    parser.add_argument(prefix + write_param_ctd, nargs='*')
    parser.add_argument(prefix + input_ctd, type=str)

    cl_arg_list = cl_args.split() if isinstance(cl_args, str) else cl_args  # string or list of args
    directives, rest = parser.parse_known_args(cl_arg_list)
    directives = vars(directives)

    transform = lambda x: None if x is None else True if x == [] else x[0]

    parsed_directives = {}
    parsed_directives['write_tool_ctd'] = transform(directives[write_tool_ctd])
    parsed_directives['write_param_ctd'] = transform(directives[write_param_ctd])
    parsed_directives['input_ctd'] = directives[input_ctd]

    return parsed_directives


# TODO: ElementTree does not provide line information... maybe refactor using lxml or other parser that does support it?
def validate_contains_keys(dictionary, keys, element_tag):
    for key in keys:
        assert key in dictionary, "Missing required attribute '%s' in %s element. Present attributes: %s" % \
                                  (key, element_tag,
                                   ', '.join(['{0}="{1}"'.format(k, v) for k, v in dictionary.items()]))