how can I use NSISO8601DateFormatOptions with PyObjC

Viewed 115

In calling NSISO8601DateFormatter to format a timestamp from NSDate, I'd like to set options for the formatting. Like NSISO8601DateFormatWithSpaceBetweenDateAndTime as described in the documentation for NSISO8601DateFormatOptions

I can't seem to how to specify the necessary import in Python, and how to apply the options. I can't find the NSISO8601DateFormatter in the library repo like with e.g. NSTimeZone but yet the NSISO8601DateFormatter import works.

I've tried the import with the commented lines, but that gives ImportError: cannot import name...

I'm testing on a Mac running macOS 11.2.3 and default python 2.7.16

Example output:

2021-04-01T12:55:41+02:00 [py_objc_timestamp] timestamp from PyObjC

Example of desired output:

2021-04-01 12:55:41+02:00 [py_objc_timestamp] timestamp from PyObjC

This is my testing code, it's working aside from the lines commented out.

#!/usr/bin/python

# coding=utf-8

import CoreFoundation
from Foundation import (NSDate,
    NSISO8601DateFormatter,
    NSTimeZone)
#from Foundation import kCFISO8601DateFormatWithSpaceBetweenDateAndTime
#from Foundation import NSISO8601DateFormatOptions


def timestamp(dt=None):
    if not dt:
        dt = NSDate.date() # use now
    the_format = NSISO8601DateFormatter.alloc().init()
    the_format.setTimeZone_(NSTimeZone.localTimeZone())
    #the_format.formatOptions = { kCFISO8601DateFormatWithSpaceBetweenDateAndTime }
    timestamp = the_format.stringFromDate_(dt)
    return timestamp

if __name__ == '__main__':
    print (timestamp() + " [py_objc_timestamp] timestamp from PyObjC")
3 Answers

disclaimer: don't have my mac on hand and can't test any of this

this, however, is the header you're looking for: https://github.com/apple/swift-corelibs-foundation/blob/main/CoreFoundation/Locale.subproj/CFDateFormatter.h#L53

these are constants.

Here's how we load these:

from Foundation import NSBundle

# Load the framework bundle by its identifier
Foundation_bundle = NSBundle.bundleWithIdentifier_("com.apple.Foundation")
objc.loadBundleVariables(Foundation_bundle, globals(), [('kCFISO8601DateFormatWithSpaceBetweenDateAndTime', '@')])

this will load the constant in as a python variable you can now use.

More info here (ctrl + f loadBundleVariables):
https://pyobjc.readthedocs.io/en/latest/metadata/manual.html

It doesn’t look like those enumerator constants are defined in PyObjC’s bridgesupport file, but their values can still be used by looking them up in the Foundation documentation. Note that all the individual pieces need to be included when creating the formatting mask:

kCFISO8601DateFormatWithFullDate = 275
kCFISO8601DateFormatWithFullTime = 1632
kCFISO8601DateFormatWithSpaceBetweenDateAndTime = 128

A bitwise operation is typically used to get the mask, but adding those gives a value of 2035.

Another issue is setting the format options. Currently that is a property, but in this case a setter method needs to be used instead (it can vary depending on the particular framework).

Putting all that together, the commented import statements are not needed, and the formatting statement (tested in Catalina and Big Sur) becomes:

the_format.setFormatOptions_(2035)

Answering my own question after getting help from krit and red_menace in the other two answers and doing some more research of my own.

The best clue I could find about not being able to import the NSISO8601DateFormatOptions constants from Foundation is that the constant I was looking for kCFISO8601DateFormatWithFractionalSeconds only appears in aliases in a PyObjC Foundation metadata file. In comparison, kCFPreferencesCurrentHost which CAN be imported from CoreFoundation appears in constants in a similar metadata file in that repo.

Maybe a future version of PyObjC will support importing these constants.

Here's my updated test code that demonstrates how we can define the different constants manually, I added all of them in case someone with similar needs comes looking here.

#!/usr/bin/python
# coding=utf-8

# File: py_objc_timestamp.py
# Demonstrates how to get ISO8601 / RFC3339 Internet timestamps
# using PyObjC bridge and Foundation framework

import CoreFoundation
from Foundation import (NSDate,
    NSISO8601DateFormatter,
    NSTimeZone)

'''
It doesn’t look like NSISO8601DateFormatOptions constants are covered by PyObjC’s
    bridgesupport file - see https://stackoverflow.com/a/66933245/4326287
They're listed not as constants or enums but as aliases in
    https://github.com/ronaldoussoren/pyobjc/blob/master/pyobjc-framework-Cocoa/Lib/CoreFoundation/_metadata.py
'''
try:
    from Foundation import (kCFISO8601DateFormatWithInternetDateTime, \
        kCFISO8601DateFormatWithFullTime)
except:
    '''
    manually defining constants for option flag bits, gleaning constants and values from
    https://github.com/apple/swift-corelibs-foundation/blob/main/CoreFoundation/Locale.subproj/CFDateFormatter.h#L53
    '''
    print("[py_objc_timestamp] import of NSISO8601DateFormatOptions constants from Foundation failed, defining manually")
    kCFISO8601DateFormatWithYear = 1 << 0
    kCFISO8601DateFormatWithMonth = 1 << 1
    kCFISO8601DateFormatWithWeekOfYear = 1 << 2
    kCFISO8601DateFormatWithDay = 1 << 4
    kCFISO8601DateFormatWithTime = 1 << 5
    kCFISO8601DateFormatWithTimeZone = 1 << 6
    kCFISO8601DateFormatWithSpaceBetweenDateAndTime = 1 << 7
    kCFISO8601DateFormatWithDashSeparatorInDate = 1 << 8
    kCFISO8601DateFormatWithColonSeparatorInTime = 1 << 9
    kCFISO8601DateFormatWithColonSeparatorInTimeZone = 1 << 10
    kCFISO8601DateFormatWithFractionalSeconds = 1 << 11
    #kCFISO8601DateFormatWithFullDate = 275
    kCFISO8601DateFormatWithFullDate = kCFISO8601DateFormatWithYear | \
        kCFISO8601DateFormatWithMonth | kCFISO8601DateFormatWithDay | \
        kCFISO8601DateFormatWithDashSeparatorInDate
    #kCFISO8601DateFormatWithFullTime = 1632
    kCFISO8601DateFormatWithFullTime = kCFISO8601DateFormatWithTime | \
        kCFISO8601DateFormatWithColonSeparatorInTime | \
        kCFISO8601DateFormatWithTimeZone | \
        kCFISO8601DateFormatWithColonSeparatorInTimeZone
    # Default - int(1907)
    kCFISO8601DateFormatWithInternetDateTime = kCFISO8601DateFormatWithFullDate | \
        kCFISO8601DateFormatWithFullTime

# define as global, need to setup only once
timestamp_format = NSISO8601DateFormatter.alloc().init()

# I'm a fan of local timezone timestamps rather than UTC, easier in testing software
timestamp_format.setTimeZone_(NSTimeZone.localTimeZone())

# Give us a Internet style, timezone-aware format, space instead of T for readability
timestamp_format_format_options = kCFISO8601DateFormatWithInternetDateTime | \
    kCFISO8601DateFormatWithSpaceBetweenDateAndTime
print("[py_objc_timestamp] NSISO8601DateFormatOptions add up to: %i" % timestamp_format_format_options)
timestamp_format.setFormatOptions_(timestamp_format_format_options)
# If you absolutely must have speed rather than readability, you can strip the
# formatting constants and hardcode the combined value instead
# timestamp_format.setFormatOptions_(2035)

def timestamp(dt=None):
    if not dt:
        dt = NSDate.date() # use now
    timestamp = timestamp_format.stringFromDate_(dt)
    return timestamp

if __name__ == '__main__':
    print (timestamp() + " [py_objc_timestamp] timestamp from PyObjC")

Example of output:

[py_objc_timestamp] import of NSISO8601DateFormatOptions constants from Foundation failed, defining manually
[py_objc_timestamp] NSISO8601DateFormatOptions add up to: 2035
2021-04-04 20:09:43+02:00 [py_objc_timestamp] timestamp from PyObjC
Related