Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# Imports
3from bs4 import BeautifulSoup
4import csv
5import io
6import logging
7import os
8import re
9from shutil import copy2
10import six
11from .constants import BASE10, BASE62, BOOLEAN_VALUES, FALSE_VALUES, TRUE_VALUES
13log = logging.getLogger(__name__)
15# Exports
17__all__ = (
18 "append_file",
19 "average",
20 "base_convert",
21 "camelcase_to_underscore",
22 "copy_file",
23 "copy_tree",
24 "indent",
25 "is_bool",
26 "is_integer",
27 "is_string",
28 "percentage",
29 "read_csv",
30 "read_file",
31 "smart_cast",
32 "strip_html_tags",
33 "to_bool",
34 "truncate",
35 "underscore_to_camelcase",
36 "underscore_to_title_case",
37 "write_file",
38 "File",
39)
41# Functions
44def append_file(path, content):
45 """Append to a file.
47 :param path: The path to the file.
48 :type path: str
50 :param content: The content to be appended the file.
51 :type content: str
53 .. code-block:: python
55 from superdjango.utils import append_file
57 append_file("path/to/readme.txt", "This is a test.")
59 """
60 with io.open(path, "a", encoding="utf-8") as f:
61 f.write(content)
62 f.close()
65def average(values):
66 """Calculate the average of a given number of values.
68 :param values: The values to be averaged.
69 :type values: list | tuple
71 :rtype: float
73 Ever get tired of creating a try/except for zero division? I do.
75 .. code-block:: python
77 from superdjango.utils import average
79 values = [1, 2, 3, 4, 5]
80 print(average(values))
82 """
83 try:
84 return float(sum(values) / len(values))
85 except ZeroDivisionError:
86 return 0.0
89def base_convert(number, from_digits=BASE10, to_digits=BASE62):
90 """Convert a number between two bases of arbitrary digits.
92 :param number: The number to be converted.
93 :type number: int
95 :param from_digits: The digits to use as the source of the conversion. ``number`` is included in these digits.
96 :type from_digits: str
98 :param to_digits: The digits to which the number will be converted.
99 :type to_digits: str
101 :rtype: str
103 """
105 if str(number)[0] == '-':
106 number = str(number)[1:]
107 negative = True
108 else:
109 negative = False
111 x = 0
112 for digit in str(number):
113 x = x * len(from_digits) + from_digits.index(digit)
115 if x == 0:
116 result = to_digits[0]
117 else:
118 result = ""
120 while x > 0:
121 digit = x % len(to_digits)
122 result = to_digits[digit] + result
123 x = int(x / len(to_digits))
125 if negative:
126 result = "-" + result
128 return result
131def camelcase_to_underscore(string):
132 """Convert a given string from ``CamelCase`` to ``camel_case``.
134 :param string: The string to be converted.
135 :type string: str
137 :rtype: str
139 """
140 # http://djangosnippets.org/snippets/585/
141 return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', '_\\1', string).lower().strip('_')
144def copy_file(from_path, to_path, make_directories=False):
145 """Copy a file from one location to another.
147 :param from_path: The source path.
148 :type from_path: str || unicode
150 :param to_path: The destination path.
151 :type to_path: str || unicode
153 :param make_directories: Create directories as needed along the ``to_path``.
154 :type make_directories: bool
156 :rtype: tuple(bool, str)
157 :returns: Success or failure and a message if failure.
159 .. code-block:: python
161 from superdjango.utils import copy_file
163 copy_file("readme-template.txt", "path/to/project/readme.txt")
165 """
166 if make_directories:
167 base_path = os.path.dirname(to_path)
168 if not os.path.exists(base_path):
169 os.makedirs(base_path)
171 try:
172 copy2(from_path, to_path)
173 return True, None
174 except IOError as e:
175 return False, str(e)
178def copy_tree(from_path, to_path):
179 """Recursively copy a source directory to a given destination.
181 :param from_path: The source directory.
182 :type from_path: str
184 :param to_path: The destination directory. This must already exist.
185 :type to_path: str
187 :rtype: bool
188 :returns: ``True`` if successful.
190 .. note::
191 Errors are logged using the Python log.
193 .. code-block:: python
195 from superdjango.utils import copy_tree
197 success = copy_tree("from/path", "to/path")
198 print(success)
200 """
201 # Deal with absolutes and user expansion.
202 source = os.path.abspath(os.path.expanduser(from_path))
203 destination = os.path.abspath(os.path.expanduser(to_path))
205 if not os.path.exists(destination):
206 log.error("Destination does not exist: %s" % destination)
207 return False
209 # Iterate through the source.
210 success = True
211 for root, dirs, files in os.walk(source):
212 directory_path = os.path.join(destination, os.path.relpath(root, source))
213 if not os.path.exists(directory_path):
214 os.mkdir(directory_path)
216 for f in files:
217 source_file = os.path.join(root, f)
218 file_path = os.path.join(directory_path, f)
220 try:
221 copy2(source_file, file_path)
222 except IOError as e:
223 success = False
224 log.warning("Could not copy %s: %s" % (source_file, e))
226 return success
229def indent(text, amount=4):
230 """Indent a string.
232 :param text: The text to be indented.
233 :type text: str
235 :param amount: The number of spaces to use for indentation.
236 :type amount: int
238 :rtype: str
240 .. code-block:: python
242 from superdjango.utils import indent
244 text = "This text will be indented."
245 print(indent(text))
247 """
248 prefix = " " * amount
249 return prefix + text.replace('\n', '\n' + prefix)
252def is_bool(value, test_values=BOOLEAN_VALUES):
253 """Determine if the given value is a boolean at run time.
255 :param value: The value to be checked.
257 :param test_values: The possible values that could be True or False.
258 :type test_values: list | tuple
260 :rtype: bool
262 .. code-block:: python
264 from superdjango.utils import is_bool
266 print(is_bool("yes"))
267 print(is_bool(True))
268 print(is_bool("No"))
269 print(is_bool(False))
271 .. note::
272 By default, a liberal number of values are used to test. If you *just* want ``True`` or ``False``, simply pass
273 ``(True, False)`` as ``test_values``.
275 """
276 return value in test_values
279def is_integer(value, cast=False):
280 """Indicates whether the given value is an integer. Saves a little typing.
282 :param value: The value to be checked.
284 :param cast: Indicates whether the value (when given as a string) should be cast to an integer.
285 :type cast: bool
287 :rtype: bool
289 .. code-block:: python
291 from superdjango.utils import is_integer
293 print(is_integer(17))
294 print(is_integer(17.5))
295 print(is_integer("17"))
296 print(is_integer("17", cast=True))
298 """
299 if isinstance(value, int):
300 return True
302 if isinstance(value, (float, str)) and cast:
303 try:
304 int(value)
305 except ValueError:
306 return False
307 else:
308 return True
310 return False
313def is_string(value):
314 """Indicates whether the given value is a string. Saves a little typing.
316 :param value: The value to be checked.
318 :rtype: bool
320 .. code-block:: python
322 from superdjango.utils import is_string
324 print(is_string("testing"))
325 print(is_string("17"))
326 print(is_string(17))
328 """
329 return isinstance(value, six.string_types)
332def percentage(portion, total):
333 """Calculate the percentage that a portion makes up of a total.
335 :param portion: The portion of the total to be calculated as a percentage.
336 :type portion: float | int
338 :param total: The total amount.
339 :type total: float | int
341 :rtype: float
343 .. code-block:: python
345 from superdjango.utils import percentage
347 p = percentage(50, 100)
348 print(p + "%")
350 """
351 try:
352 return 100.0 * portion / total
353 except (TypeError, ZeroDivisionError):
354 return 0.0
357def read_csv(path, encoding="utf-8", first_row_field_names=False):
358 """Read the contents of a CSV file.
360 :param path: The path to the file.
361 :type path: str
363 :param encoding: The encoding of the file.
364 :type encoding: str
366 :param first_row_field_names: Indicates the first row contains the field names. In this case the returned rows will
367 be a dictionary rather than a list.
369 :type first_row_field_names: bool
371 :rtype: list[list] || list[dict]
373 .. code-block:: text
375 menu,identifier,sort_order,text,url
376 main,product,10,Product,/product/
377 main,solutions,20,Solutions,/solutions/
378 main,resources,30,Resources,/resources/
379 main,support,40,Support,https://support.example.com
380 main,about,50,About,/about/
381 main,contact,60,Contact,/contact/
383 .. code-block:: python
385 from superdjango.utils import read_csv
387 rows = read_csv("path/to/menus.csv", first_row_fields_names=True)
388 for r in rows:
389 print("%s: %s" % (row['identifier'], row['url']
391 """
392 with io.open(path, "r", encoding=encoding) as f:
393 if first_row_field_names:
394 reader = csv.DictReader(f)
395 else:
396 reader = csv.reader(f)
398 rows = list()
399 for row in reader:
400 rows.append(row)
402 f.close()
404 return rows
407def read_file(path):
408 """Read a file and return its contents.
410 :param path: The path to the file.
411 :type path: str || unicode
413 :rtype: str
415 .. code-block:: python
417 from superdjango.utils import read_file
419 output = read_file("path/to/readme.txt")
420 print(output)
422 """
423 with io.open(path, "r", encoding="utf-8") as f:
424 output = f.read()
425 f.close()
427 return output
430def smart_cast(value):
431 """Intelligently cast the given value to a Python data type.
433 :param value: The value to be cast.
434 :type value: str
436 """
437 # Handle integers first because is_bool() may interpret 0s and 1s ad booleans.
438 if is_integer(value, cast=True):
439 return int(value)
440 elif is_bool(value):
441 return to_bool(value)
442 else:
443 return value
446def strip_html_tags(html):
447 """Strip HTML tags from a string.
449 :param html: The string from which HTML tags should be stripped.
450 :type html: str | unicode
452 :rtype: str
454 .. code-block:: python
456 from superdjango.utils import strip_html_tags
458 html = "<p>This string contains <b>HTML</b> tags.</p>"
459 print(strip_html_tags(html))
461 """
462 return "".join(BeautifulSoup(html, "html.parser").find_all(text=True))
465def to_bool(value, false_values=FALSE_VALUES, true_values=TRUE_VALUES):
466 """Convert the given value to it's boolean equivalent.
468 :param value: The value to be converted.
470 :param false_values: The possible values that could be False.
471 :type false_values: list | tuple
473 :param true_values: The possible values that could be True.
474 :type true_values: list | tuple
476 :rtype: bool
478 :raises: ``ValueError`` if the value could not be converted.
480 .. code-block:: python
482 from superdjango.utils import to_bool
484 print(to_bool("yes"))
485 print(to_bool(1))
486 print(to_bool("no"))
487 print(to_bool(0))
489 """
490 if value in true_values:
491 return True
493 if value in false_values:
494 return False
496 raise ValueError('"%s" cannot be converted to True or False.')
499def truncate(string, continuation="...", limit=30):
500 """Get a truncated version of a string if if over the limit.
502 :param string: The string to be truncated.
503 :type string: str || None
505 :param limit: The maximum number of characters.
506 :type limit: int
508 :param continuation: The string to add to the truncated title.
509 :type continuation: str || None
511 :rtype: str
513 .. code-block:: python
515 from superdjango.utils import truncate
517 title = "This Title is Too Long to Be Displayed As Is"
518 print(truncate(title))
520 """
521 # Make it safe to submit the string as None.
522 if string is None:
523 return ""
525 # There's nothing to do if the string is not over the limit.
526 if len(string) <= limit:
527 return string
529 # Adjust the limit according to the string length, otherwise we'll still be over.
530 if continuation:
531 limit -= len(continuation)
533 # Return the altered title.
534 if continuation:
535 return string[:limit] + continuation
536 else:
537 return string[:limit]
540def underscore_to_camelcase(string):
541 """Convert a string with underscore separations to CamelCase.
543 :param string: The string to be converted.
544 :type string: str
546 :rtype: str
548 """
549 return string.replace("_", " ").title().replace(" ", "")
552def underscore_to_title_case(string):
553 """Convert a string to title case.
555 :param string: The string to be converted.
556 :type string: str
558 :rtype: str
560 """
561 return string.replace("_", " ").title()
564def write_file(path, content="", make_directories=False):
565 """Write a file.
567 :param path: The path to the file.
568 :type path: str || unicode
570 :param content: The content of the file. An empty string is effectively the same as a "touch".
571 :type content: str || unicode
573 :param make_directories: Create directories as needed along the file path.
574 :type make_directories: bool
576 .. code-block:: python
578 from superdjango.utils import write_file
580 write_file("path/to/readme.txt", "This is a test.")
582 """
583 if make_directories:
584 base_path = os.path.dirname(path)
585 if not os.path.exists(base_path):
586 os.makedirs(base_path)
588 with io.open(path, "w", encoding="utf-8") as f:
589 f.write(content)
590 f.close()
593# Classes
596class File(object):
597 """A simple helper class for working with file names.
599 For more robust handling of paths, see `pathlib`_.
601 .. _pathlib: https://docs.python.org/3/library/pathlib.html
603 """
605 def __init__(self, path):
606 """Initialize the file instance.
608 :param path: The path to the file.
609 :type path: str
611 """
612 self.basename = os.path.basename(path)
613 self.directory = os.path.dirname(path)
614 self.extension = os.path.splitext(path)[-1]
615 self.name = os.path.basename(os.path.splitext(path)[0])
616 self.path = path
618 def __repr__(self):
619 return "<%s %s>" % (self.__class__.__name__, self.basename)
621 @property
622 def exists(self):
623 """Indicates the file exists.
625 :rtype: bool
627 """
628 return os.path.exists(self.path)