8000 Merge pull request #1 from aignas/pep440-suggestions · vonschultz/rules_python@6816bd4 · GitHub
[go: up one dir, main page]

Skip to content

Commit 6816bd4

Browse files
authored
Merge pull request #1 from aignas/pep440-suggestions
PEP440 normalization code structure suggestions
2 parents f6695f7 + 1076fd7 commit 6816bd4

File tree

3 files changed

+405
-386
lines changed

3 files changed

+405
-386
lines changed

python/private/py_wheel.bzl

Lines changed: 1 addition & 385 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
load("//python/private:stamp.bzl", "is_stamping_enabled")
1818
load(":py_package.bzl", "py_package_lib")
19+
load(":py_wheel_normalize_pep440.bzl", "normalize_pep440")
1920

2021
PyWheelInfo = provider(
2122
doc = "Information about a wheel produced by `py_wheel`",
@@ -239,391 +240,6 @@ def _escape_filename_distribution_name(name):
239240
)
240241
return escaped
241242

242-
def normalize_pep440(version):
243-
"""Escape the version component of a filename.
244-
245-
See https://packaging.python.org/en/latest/specifications/binary-distribution-format/#escaping-and-unicode
246-
and https://peps.python.org/pep-0440/
247-
248-
Args:
249-
version: version string to be normalized according to PEP 440.
250-
251-
Returns:
252-
string containing the normalized version.
253-
"""
254-
255-
version = version.strip() # PEP 440: Leading and Trailing Whitespace
256-
contexts = []
257-
258-
def open_context(start):
259-
"""Open an new parsing context.
260-
261-
If the current parsing step succeeds, call close_context().
262-
If the current parsing step fails, call contexts.pop() to
263-
go back to how it was before we opened a new context.
264-
265-
Args:
266-
start: index into `version` indicating where the current
267-
parsing step starts.
268-
"""
269-
contexts.append({"norm": "", "start": start})
270-
return contexts[-1]
271-
272-
def close_context():
273-
"""Close the current context successfully and merge the results."""
274-
finished = contexts.pop()
275-
contexts[-1]["norm"] += finished["norm"]
276-
contexts[-1]["start"] = finished["start"]
277-
278-
def is_(reference):
279-
"""Predicate testing a token for equality with `reference`."""
280-
return lambda token: token == reference
281-
282-
def is_not(reference):
283-
"""Predicate testing a token for inequality with `reference`."""
284-
return lambda token: token != reference
285-
286-
def in_(reference):
287-
"""Predicate testing if a token is in the list `reference`."""
288-
return lambda token: token in reference
289-
290-
def accept(predicate, value):
291-
"""If `predicate` matches the next token, accept the token.
292-
293-
Accepting the token means adding it (according to `value`) to
294-
the running results maintained in context["norm"] and
295-
advancing the cursor in context["start"] to the next token in
296-
`version`.
297-
298-
Args:
299-
predicate: function taking a token and returning a boolean
300-
saying if we want to accept the token.
301-
value: the string to add if there's a match, or, if `value`
302-
is a function, the function to apply to the current token
303-
to get the string to add.
304-
305-
Returns:
306-
whether a token was accepted.
307-
"""
308-
309-
context = contexts[-1]
310-
311-
if context["start"] >= len(version):
312-
return False
313-
314-
token = version[context["start"]]
315-
316-
if predicate(token):
317-
if type(value) in ["function", "builtin_function_or_method"]:
318-
value = value(token)
319-
320-
context["norm"] += value
321-
context["start"] += 1
322-
return True
323-
324-
return False
325-
326-
def accept_placeholder():
327-
"""Accept a Bazel placeholder.
328-
329-
Placeholders aren't actually part of PEP 440, but are used for
330-
stamping purposes. A placeholder might be
331-
``{BUILD_TIMESTAMP}``, for instance. We'll accept these as
332-
they are, assuming they will expand to something that makes
333-
sense where they appear. Before the stamping has happened, a
334-
resulting wheel file name containing a placeholder will not
335-
actually be valid.
336-
337-
"""
338-
context = open_context(contexts[-1]["start"])
339-
340-
if not accept(is_("{"), str):
341-
contexts.pop()
342-
return False
343-
344-
start = context["start"]
345-
for _ in range(start, len(version) + 1):
346-
if not accept(is_not("}"), str):
347-
break
348-
349-
if not accept(is_("}"), str):
350-
contexts.pop()
351-
return False
352-
353-
close_context()
354-
return True
355-
356-
def accept_digits():
357-
"""Accept multiple digits (or placeholders)."""
358-
359-
def isdigit(token):
360-
return token.isdigit()
361-
362-
context = open_context(contexts[-1]["start"])
363-
start = context["start"]
364-
365-
for i in range(start, len(version) + 1):
366-
if not accept(isdigit, str) and not accept_placeholder():
367-
if i - start >= 1:
368-
if context["norm"].isdigit():
369-
# PEP 440: Integer Normalization
370-
context["norm"] = str(int(context["norm"]))
371-
close_context()
372-
return True
373-
break
374-
375-
contexts.pop()
376-
return False
377-
378-
def accept_string(string, replacement):
379-
"""Accept a `string` in the input. Output `replacement`."""
380-
context = open_context(contexts[-1]["start"])
381-
382-
for character in string.elems():
383-
if not accept(in_([character, character.upper()]), ""):
384-
contexts.pop()
385-
return False
386-
387-
context["norm"] = replacement
388-
389-
close_context()
390-
return True
391-
392-
def accept_alnum():
393-
"""Accept an alphanumeric sequence."""
394-
395-
def isalnum(token):
396-
return token.isalnum()
397-
398-
# PEP 440: Case sensitivity
399-
def lower(token):
400-
return token.lower()
401-
402-
context = open_context(contexts[-1]["start"])
403-
start = context["start"]
404-
405-
for i in range(start, len(version) + 1):
406-
if not accept(isalnum, lower) and not accept_placeholder():
407-
if i - start >= 1:
408-
close_context()
409-
return True
410-
break
411-
412-
contexts.pop()
413-
return False
414-
415-
def accept_dot_number():
416-
"""Accept a dot followed by digits."""
417-
open_context(contexts[-1]["start"])
418-
419-
if accept(is_("."), ".") and accept_digits():
420-
close_context()
421-
return True
422-
else:
423-
contexts.pop()
424-
return False
425-
426-
def accept_dot_number_sequence():
427-
"""Accept a sequence of dot+digits."""
428-
context = contexts[-1]
429-
start = context["start"]
430-
i = start
431-
432-
for i in range(start, len(version) + 1):
433-
if not accept_dot_number():
434-
break
435-
return i - start >= 1
436-
437-
def accept_separator_alnum():
438-
"""Accept a separator followed by an alphanumeric string."""
439-
open_context(contexts[-1]["start"])
440-
441-
# PEP 440: Local version segments
442-
if (
443-
accept(in_([".", "-", "_"]), ".") and
444-
(accept_digits() or accept_alnum())
445-
):
446-
close_context()
447-
return True
448-
449-
contexts.pop()
450-
return False
451-
452-
def accept_separator_alnum_sequence():
453-
"""Accept a sequence of separator+alphanumeric."""
454-
context = contexts[-1]
455-
start = context["start"]
456-
i = start
457-
458-
for i in range(start, len(version) + 1):
459-
if not accept_separator_alnum():
460-
break
461-
462-
return i - start >= 1
463-
464-
def accept_epoch():
465-
"""PEP 440: Version epochs."""
466-
context = open_context(contexts[-1]["start"])
467-
if accept_digits() and accept(is_("!"), "!"):
468-
if context["norm"] == "0!":
469-
contexts.pop()
470-
contexts[-1]["start"] = context["start"]
471-
else:
472-
close_context()
473-
return True
474-
else:
475-
contexts.pop()
476-
return False
477-
478-
def accept_release():
479-
"""Accept the release segment, numbers separated by dots."""
480-
open_context(contexts[-1]["start"])
481-
482-
if not accept_digits():
483-
contexts.pop()
484-
return False
485-
486-
accept_dot_number_sequence()
487-
close_context()
488-
return True
489-
490-
def accept_pre_l():
491-
"""PEP 440: Pre-release spelling."""
492-
open_context(contexts[-1]["start"])
493-
494-
if (
495-
accept_string("alpha", "a") or
496-
accept_string("a", "a") or
497-
accept_string("beta", "b") or
498-
accept_string("b", "b") or
499-
accept_string("c", "rc") or
500-
accept_string("preview", "rc") or
501-
accept_string("pre", "rc") or
502-
accept_string("rc", "rc")
503-
):
504-
close_context()
505-
return True
506-
else:
507-
contexts.pop()
508-
return False
509-
510-
def accept_prerelease():
511-
"""PEP 440: Pre-releases."""
512-
context = open_context(contexts[-1]["start"])
513-
514-
# PEP 440: Pre-release separators
515-
accept(in_(["-", "_", "."]), "")
516-
517-
if not accept_pre_l():
518-
contexts.pop()
519-
return False
520-
521-
accept(in_(["-", "_", "."]), "")
522-
523-
if not accept_digits():
524-
# PEP 440: Implicit pre-release number
525-
context["norm"] += "0"
526-
527-
close_context()
528-
return True
529-
530-
def accept_implicit_postrelease():
531-
"""PEP 440: Implicit post releases."""
532-
context = open_context(contexts[-1]["start"])
533-
534-
if accept(is_("-"), "") and accept_digits():
535-
context["norm"] = ".post" + context["norm"]
536-
close_context()
537-
return True
538-
539-
contexts.pop()
540-
return False
541-
542-
def accept_explicit_postrelease():
543-
"""PEP 440: Post-releases."""
544-
context = open_context(contexts[-1]["start"])
545-
546-
# PEP 440: Post release separators
547-
if not accept(in_(["-", "_", "."]), "."):
548-
context["norm"] += "."
549-
550-
# PEP 440: Post release spelling
551-
if (
552-
accept_string("post", "post") or
553-
accept_string("rev", "post") or
554-
accept_string("r", "post")
555-
):
556-
accept(in_(["-", "_", "."]), "")
557-
558-
if not accept_digits():
559-
# PEP 440: Implicit post release number
560-
context["norm"] += "0"
561-
562-
close_context()
563-
return True
564-
565-
contexts.pop()
566-
return False
567-
568-
def accept_postrelease():
569-
"""PEP 440: Post-releases."""
570-
open_context(contexts[-1]["start"])
571-
572-
if accept_implicit_postrelease() or accept_explicit_postrelease():
573-
close_context()
574-
return True
575-
576-
contexts.pop()
577-
return False
578-
579-
def accept_devrelease():
580-
"""PEP 440: Developmental releases."""
581-
context = open_context(contexts[-1]["start"])
582-
583-
# PEP 440: Development release separators
584-
if not accept(in_(["-", "_", "."]), "."):
585-
context["norm"] += "."
586-
587-
if accept_string("dev", "dev"):
588-
accept(in_(["-", "_", "."]), "")
589-
590-
if not accept_digits():
591-
# PEP 440: Implicit development release number
592-
context["norm"] += "0"
593-
594-
close_context()
595-
return True
596-
597-
contexts.pop()
598-
return False
599-
600-
def accept_local():
601-
"""PEP 440: Local version identifiers."""
602-
open_context(contexts[-1]["start"])
603-
604-
if accept(is_("+"), "+") and accept_alnum():
605-
accept_separator_alnum_sequence()
606-
close_context()
607-
return True
608-
609-
contexts.pop()
610-
return False
611-
612-
open_context(0)
613-
accept(is_("v"), "") # PEP 440: Preceding v character
614-
accept_epoch()
615-
accept_release()
616-
accept_prerelease()
617-
accept_postrelease()
618-
accept_devrelease()
619-
accept_local()
620-
if version[contexts[-1]["start"]:]:
621-
fail(
622-
"Failed to parse PEP 440 version identifier '%s'." % version,
623-
"Parse error at '%s'" % version[contexts[-1]["start"]:],
624-
)
625-
return contexts[-1]["norm"]
626-
627243
def _escape_filename_segment(segment):
628244
"""Escape a segment of the wheel filename.
629245

0 commit comments

Comments
 (0)
0