Hide keyboard shortcuts

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 

2 

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 

12 

13log = logging.getLogger(__name__) 

14 

15# Exports 

16 

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) 

40 

41# Functions 

42 

43 

44def append_file(path, content): 

45 """Append to a file. 

46 

47 :param path: The path to the file. 

48 :type path: str 

49 

50 :param content: The content to be appended the file. 

51 :type content: str 

52 

53 .. code-block:: python 

54 

55 from superdjango.utils import append_file 

56 

57 append_file("path/to/readme.txt", "This is a test.") 

58 

59 """ 

60 with io.open(path, "a", encoding="utf-8") as f: 

61 f.write(content) 

62 f.close() 

63 

64 

65def average(values): 

66 """Calculate the average of a given number of values. 

67 

68 :param values: The values to be averaged. 

69 :type values: list | tuple 

70 

71 :rtype: float 

72 

73 Ever get tired of creating a try/except for zero division? I do. 

74 

75 .. code-block:: python 

76 

77 from superdjango.utils import average 

78 

79 values = [1, 2, 3, 4, 5] 

80 print(average(values)) 

81 

82 """ 

83 try: 

84 return float(sum(values) / len(values)) 

85 except ZeroDivisionError: 

86 return 0.0 

87 

88 

89def base_convert(number, from_digits=BASE10, to_digits=BASE62): 

90 """Convert a number between two bases of arbitrary digits. 

91 

92 :param number: The number to be converted. 

93 :type number: int 

94 

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 

97 

98 :param to_digits: The digits to which the number will be converted. 

99 :type to_digits: str 

100 

101 :rtype: str 

102 

103 """ 

104 

105 if str(number)[0] == '-': 

106 number = str(number)[1:] 

107 negative = True 

108 else: 

109 negative = False 

110 

111 x = 0 

112 for digit in str(number): 

113 x = x * len(from_digits) + from_digits.index(digit) 

114 

115 if x == 0: 

116 result = to_digits[0] 

117 else: 

118 result = "" 

119 

120 while x > 0: 

121 digit = x % len(to_digits) 

122 result = to_digits[digit] + result 

123 x = int(x / len(to_digits)) 

124 

125 if negative: 

126 result = "-" + result 

127 

128 return result 

129 

130 

131def camelcase_to_underscore(string): 

132 """Convert a given string from ``CamelCase`` to ``camel_case``. 

133 

134 :param string: The string to be converted. 

135 :type string: str 

136 

137 :rtype: str 

138 

139 """ 

140 # http://djangosnippets.org/snippets/585/ 

141 return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', '_\\1', string).lower().strip('_') 

142 

143 

144def copy_file(from_path, to_path, make_directories=False): 

145 """Copy a file from one location to another. 

146 

147 :param from_path: The source path. 

148 :type from_path: str || unicode 

149 

150 :param to_path: The destination path. 

151 :type to_path: str || unicode 

152 

153 :param make_directories: Create directories as needed along the ``to_path``. 

154 :type make_directories: bool 

155 

156 :rtype: tuple(bool, str) 

157 :returns: Success or failure and a message if failure. 

158 

159 .. code-block:: python 

160 

161 from superdjango.utils import copy_file 

162 

163 copy_file("readme-template.txt", "path/to/project/readme.txt") 

164 

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) 

170 

171 try: 

172 copy2(from_path, to_path) 

173 return True, None 

174 except IOError as e: 

175 return False, str(e) 

176 

177 

178def copy_tree(from_path, to_path): 

179 """Recursively copy a source directory to a given destination. 

180 

181 :param from_path: The source directory. 

182 :type from_path: str 

183 

184 :param to_path: The destination directory. This must already exist. 

185 :type to_path: str 

186 

187 :rtype: bool 

188 :returns: ``True`` if successful. 

189 

190 .. note:: 

191 Errors are logged using the Python log. 

192 

193 .. code-block:: python 

194 

195 from superdjango.utils import copy_tree 

196 

197 success = copy_tree("from/path", "to/path") 

198 print(success) 

199 

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)) 

204 

205 if not os.path.exists(destination): 

206 log.error("Destination does not exist: %s" % destination) 

207 return False 

208 

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) 

215 

216 for f in files: 

217 source_file = os.path.join(root, f) 

218 file_path = os.path.join(directory_path, f) 

219 

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)) 

225 

226 return success 

227 

228 

229def indent(text, amount=4): 

230 """Indent a string. 

231 

232 :param text: The text to be indented. 

233 :type text: str 

234 

235 :param amount: The number of spaces to use for indentation. 

236 :type amount: int 

237 

238 :rtype: str 

239 

240 .. code-block:: python 

241 

242 from superdjango.utils import indent 

243 

244 text = "This text will be indented." 

245 print(indent(text)) 

246 

247 """ 

248 prefix = " " * amount 

249 return prefix + text.replace('\n', '\n' + prefix) 

250 

251 

252def is_bool(value, test_values=BOOLEAN_VALUES): 

253 """Determine if the given value is a boolean at run time. 

254 

255 :param value: The value to be checked. 

256 

257 :param test_values: The possible values that could be True or False. 

258 :type test_values: list | tuple 

259 

260 :rtype: bool 

261 

262 .. code-block:: python 

263 

264 from superdjango.utils import is_bool 

265 

266 print(is_bool("yes")) 

267 print(is_bool(True)) 

268 print(is_bool("No")) 

269 print(is_bool(False)) 

270 

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``. 

274 

275 """ 

276 return value in test_values 

277 

278 

279def is_integer(value, cast=False): 

280 """Indicates whether the given value is an integer. Saves a little typing. 

281 

282 :param value: The value to be checked. 

283 

284 :param cast: Indicates whether the value (when given as a string) should be cast to an integer. 

285 :type cast: bool 

286 

287 :rtype: bool 

288 

289 .. code-block:: python 

290 

291 from superdjango.utils import is_integer 

292 

293 print(is_integer(17)) 

294 print(is_integer(17.5)) 

295 print(is_integer("17")) 

296 print(is_integer("17", cast=True)) 

297 

298 """ 

299 if isinstance(value, int): 

300 return True 

301 

302 if isinstance(value, (float, str)) and cast: 

303 try: 

304 int(value) 

305 except ValueError: 

306 return False 

307 else: 

308 return True 

309 

310 return False 

311 

312 

313def is_string(value): 

314 """Indicates whether the given value is a string. Saves a little typing. 

315 

316 :param value: The value to be checked. 

317 

318 :rtype: bool 

319 

320 .. code-block:: python 

321 

322 from superdjango.utils import is_string 

323 

324 print(is_string("testing")) 

325 print(is_string("17")) 

326 print(is_string(17)) 

327 

328 """ 

329 return isinstance(value, six.string_types) 

330 

331 

332def percentage(portion, total): 

333 """Calculate the percentage that a portion makes up of a total. 

334 

335 :param portion: The portion of the total to be calculated as a percentage. 

336 :type portion: float | int 

337 

338 :param total: The total amount. 

339 :type total: float | int 

340 

341 :rtype: float 

342 

343 .. code-block:: python 

344 

345 from superdjango.utils import percentage 

346 

347 p = percentage(50, 100) 

348 print(p + "%") 

349 

350 """ 

351 try: 

352 return 100.0 * portion / total 

353 except (TypeError, ZeroDivisionError): 

354 return 0.0 

355 

356 

357def read_csv(path, encoding="utf-8", first_row_field_names=False): 

358 """Read the contents of a CSV file. 

359 

360 :param path: The path to the file. 

361 :type path: str 

362 

363 :param encoding: The encoding of the file. 

364 :type encoding: str 

365 

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. 

368 

369 :type first_row_field_names: bool 

370 

371 :rtype: list[list] || list[dict] 

372 

373 .. code-block:: text 

374 

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/ 

382 

383 .. code-block:: python 

384 

385 from superdjango.utils import read_csv 

386 

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'] 

390 

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) 

397 

398 rows = list() 

399 for row in reader: 

400 rows.append(row) 

401 

402 f.close() 

403 

404 return rows 

405 

406 

407def read_file(path): 

408 """Read a file and return its contents. 

409 

410 :param path: The path to the file. 

411 :type path: str || unicode 

412 

413 :rtype: str 

414 

415 .. code-block:: python 

416 

417 from superdjango.utils import read_file 

418 

419 output = read_file("path/to/readme.txt") 

420 print(output) 

421 

422 """ 

423 with io.open(path, "r", encoding="utf-8") as f: 

424 output = f.read() 

425 f.close() 

426 

427 return output 

428 

429 

430def smart_cast(value): 

431 """Intelligently cast the given value to a Python data type. 

432 

433 :param value: The value to be cast. 

434 :type value: str 

435 

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 

444 

445 

446def strip_html_tags(html): 

447 """Strip HTML tags from a string. 

448 

449 :param html: The string from which HTML tags should be stripped. 

450 :type html: str | unicode 

451 

452 :rtype: str 

453 

454 .. code-block:: python 

455 

456 from superdjango.utils import strip_html_tags 

457 

458 html = "<p>This string contains <b>HTML</b> tags.</p>" 

459 print(strip_html_tags(html)) 

460 

461 """ 

462 return "".join(BeautifulSoup(html, "html.parser").find_all(text=True)) 

463 

464 

465def to_bool(value, false_values=FALSE_VALUES, true_values=TRUE_VALUES): 

466 """Convert the given value to it's boolean equivalent. 

467 

468 :param value: The value to be converted. 

469 

470 :param false_values: The possible values that could be False. 

471 :type false_values: list | tuple 

472 

473 :param true_values: The possible values that could be True. 

474 :type true_values: list | tuple 

475 

476 :rtype: bool 

477 

478 :raises: ``ValueError`` if the value could not be converted. 

479 

480 .. code-block:: python 

481 

482 from superdjango.utils import to_bool 

483 

484 print(to_bool("yes")) 

485 print(to_bool(1)) 

486 print(to_bool("no")) 

487 print(to_bool(0)) 

488 

489 """ 

490 if value in true_values: 

491 return True 

492 

493 if value in false_values: 

494 return False 

495 

496 raise ValueError('"%s" cannot be converted to True or False.') 

497 

498 

499def truncate(string, continuation="...", limit=30): 

500 """Get a truncated version of a string if if over the limit. 

501 

502 :param string: The string to be truncated. 

503 :type string: str || None 

504 

505 :param limit: The maximum number of characters. 

506 :type limit: int 

507 

508 :param continuation: The string to add to the truncated title. 

509 :type continuation: str || None 

510 

511 :rtype: str 

512 

513 .. code-block:: python 

514 

515 from superdjango.utils import truncate 

516 

517 title = "This Title is Too Long to Be Displayed As Is" 

518 print(truncate(title)) 

519 

520 """ 

521 # Make it safe to submit the string as None. 

522 if string is None: 

523 return "" 

524 

525 # There's nothing to do if the string is not over the limit. 

526 if len(string) <= limit: 

527 return string 

528 

529 # Adjust the limit according to the string length, otherwise we'll still be over. 

530 if continuation: 

531 limit -= len(continuation) 

532 

533 # Return the altered title. 

534 if continuation: 

535 return string[:limit] + continuation 

536 else: 

537 return string[:limit] 

538 

539 

540def underscore_to_camelcase(string): 

541 """Convert a string with underscore separations to CamelCase. 

542 

543 :param string: The string to be converted. 

544 :type string: str 

545 

546 :rtype: str 

547 

548 """ 

549 return string.replace("_", " ").title().replace(" ", "") 

550 

551 

552def underscore_to_title_case(string): 

553 """Convert a string to title case. 

554 

555 :param string: The string to be converted. 

556 :type string: str 

557 

558 :rtype: str 

559 

560 """ 

561 return string.replace("_", " ").title() 

562 

563 

564def write_file(path, content="", make_directories=False): 

565 """Write a file. 

566 

567 :param path: The path to the file. 

568 :type path: str || unicode 

569 

570 :param content: The content of the file. An empty string is effectively the same as a "touch". 

571 :type content: str || unicode 

572 

573 :param make_directories: Create directories as needed along the file path. 

574 :type make_directories: bool 

575 

576 .. code-block:: python 

577 

578 from superdjango.utils import write_file 

579 

580 write_file("path/to/readme.txt", "This is a test.") 

581 

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) 

587 

588 with io.open(path, "w", encoding="utf-8") as f: 

589 f.write(content) 

590 f.close() 

591 

592 

593# Classes 

594 

595 

596class File(object): 

597 """A simple helper class for working with file names. 

598 

599 For more robust handling of paths, see `pathlib`_. 

600 

601 .. _pathlib: https://docs.python.org/3/library/pathlib.html 

602 

603 """ 

604 

605 def __init__(self, path): 

606 """Initialize the file instance. 

607 

608 :param path: The path to the file. 

609 :type path: str 

610 

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 

617 

618 def __repr__(self): 

619 return "<%s %s>" % (self.__class__.__name__, self.basename) 

620 

621 @property 

622 def exists(self): 

623 """Indicates the file exists. 

624 

625 :rtype: bool 

626 

627 """ 

628 return os.path.exists(self.path)