1import re
2
3from typing import Any
4from typing import Optional
5
6from clikit.utils._compat import basestring
7from clikit.utils.string import parse_boolean
8from clikit.utils.string import parse_float
9from clikit.utils.string import parse_int
10from clikit.utils.string import parse_string
11
12
13class Argument(object):
14    """
15    An input argument.
16    """
17
18    REQUIRED = 1
19    OPTIONAL = 2
20    MULTI_VALUED = 4
21    STRING = 16
22    BOOLEAN = 32
23    INTEGER = 64
24    FLOAT = 128
25    NULLABLE = 256
26
27    def __init__(
28        self, name, flags=0, description=None, default=None
29    ):  # type: (str, int, Optional[str], Any) -> None
30        if not isinstance(name, basestring):
31            raise ValueError(
32                "The argument name must be a string. Got: {}".format(type(name))
33            )
34
35        if not name:
36            raise ValueError("The argument name must not be empty.")
37
38        if not name[:1].isalpha():
39            raise ValueError("The argument name must start with a letter")
40
41        if not re.match(r"^[a-zA-Z0-9\-]+$", name):
42            raise ValueError(
43                "The argument name must contain letters, digits and hyphens only."
44            )
45
46        if description is not None:
47            if not isinstance(description, basestring):
48                raise ValueError(
49                    "The argument description must be a string. Got: {}".format(
50                        type(description)
51                    )
52                )
53
54            if not description:
55                raise ValueError("The argument description must not be empty.")
56
57        self._validate_flags(flags)
58
59        flags = self._add_default_flags(flags)
60
61        self._name = name
62        self._flags = flags
63        self._description = description
64
65        if self.is_multi_valued():
66            self._default = []
67        else:
68            self._default = None
69
70        if self.is_optional() or default is not None:
71            self.set_default(default)
72
73    @property
74    def name(self):  # type: () -> str
75        return self._name
76
77    @property
78    def flags(self):  # type: () -> int
79        return self._flags
80
81    @property
82    def description(self):  # type: () -> Optional[str]
83        return self._description
84
85    @property
86    def default(self):  # type: () -> Any
87        return self._default
88
89    def is_required(self):  # type: () -> bool
90        return bool(self.REQUIRED & self._flags)
91
92    def is_optional(self):  # type: () -> bool
93        return bool(self.OPTIONAL & self._flags)
94
95    def is_multi_valued(self):  # type: () -> bool
96        return bool(self.MULTI_VALUED & self._flags)
97
98    def set_default(self, default=None):  # type: (Any) -> None
99        if self.is_required():
100            raise ValueError("Required arguments do not accept default values.")
101
102        if self.is_multi_valued():
103            if default is None:
104                default = []
105            elif not isinstance(default, list):
106                raise ValueError(
107                    "The default value of a multi-valued argument must be a list. "
108                    "Got: {}".format(type(default))
109                )
110
111        self._default = default
112
113    def parse(self, value):  # type: (Any) -> Any
114        nullable = bool(self._flags & self.NULLABLE)
115
116        if self._flags & self.BOOLEAN:
117            return parse_boolean(value, nullable)
118        elif self._flags & self.INTEGER:
119            return parse_int(value, nullable)
120        elif self._flags & self.FLOAT:
121            return parse_float(value, nullable)
122
123        return parse_string(value, nullable)
124
125    def _validate_flags(self, flags):  # type: (int) -> None
126        if flags & self.REQUIRED and flags & self.OPTIONAL:
127            raise ValueError(
128                "The argument flags REQUIRED and OPTIONAL cannot be combined."
129            )
130
131        if flags & self.STRING:
132            if flags & self.BOOLEAN:
133                raise ValueError(
134                    "The argument flags STRING and BOOLEAN cannot be combined."
135                )
136
137            if flags & self.INTEGER:
138                raise ValueError(
139                    "The argument flags STRING and INTEGER cannot be combined."
140                )
141
142            if flags & self.FLOAT:
143                raise ValueError(
144                    "The argument flags STRING and FLOAT cannot be combined."
145                )
146        elif flags & self.BOOLEAN:
147            if flags & self.INTEGER:
148                raise ValueError(
149                    "The argument flags BOOLEAN and INTEGER cannot be combined."
150                )
151
152            if flags & self.FLOAT:
153                raise ValueError(
154                    "The argument flags BOOLEAN and FLOAT cannot be combined."
155                )
156        elif flags & self.INTEGER:
157            if flags & self.FLOAT:
158                raise ValueError(
159                    "The argument flags INTEGER and FLOAT cannot be combined."
160                )
161
162    def _add_default_flags(self, flags):  # type: (int) -> int
163        if not flags & (self.REQUIRED | self.OPTIONAL):
164            flags |= self.OPTIONAL
165
166        if not flags & (self.STRING | self.BOOLEAN | self.INTEGER | self.FLOAT):
167            flags |= self.STRING
168
169        return flags
170